{"repo": "agenticmail/enterprise", "files": {"src/admin/routes.ts": {"93ae0779fb38f5ddc3c69ef3b210fc8fd88e65b7": {"t": 1785746553.4038353, "n": 274715, "h": "31900bdc7060f05c", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // Two credential shapes:\n // - Standard API key (sk-ant-api…) → x-api-key header.\n // - OAuth access token (sk-ant-oat…) → Authorization: Bearer +\n // the oauth/claude-code beta headers. These come from a Claude\n // subscription / Claude Code and are REJECTED (401) by the\n // x-api-key header, so probing with x-api-key falsely flagged a\n // perfectly valid token as \"Invalid API key\". Mirror exactly\n // what the runtime's callAnthropic() does so validation agrees\n // with actual call-time behaviour.\n var isOAuthToken = apiKey.includes('sk-ant-oat');\n var anthropicHeaders: Record = isOAuthToken\n ? {\n 'authorization': 'Bearer ' + apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',\n 'user-agent': 'claude-cli/1.0.0 (external, cli)',\n 'x-app': 'cli',\n 'content-type': 'application/json',\n }\n : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' };\n var anthropicBody: Record = { model: 'claude-haiku-4-5-20251001', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] };\n // OAuth tokens are gated on the Claude Code identity as the first\n // system block (same requirement as the runtime) — include it so\n // the probe matches real call-time behaviour.\n if (isOAuthToken) {\n anthropicBody.system = [{ type: 'text', text: \"You are Claude Code, Anthropic's official CLI for Claude.\" }];\n }\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: anthropicHeaders,\n body: JSON.stringify(anthropicBody),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid creds, bad request) = creds work; 401/403 = bad creds\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n // Merge engine identity.role (source of truth) into admin agent records\n try {\n const edb = db.getEngineDB?.();\n if (edb) {\n const managed = await edb.all(`SELECT id, config FROM managed_agents`) || [];\n const roleMap = new Map();\n for (const m of managed as any[]) {\n const cfg = typeof m.config === 'string' ? JSON.parse(m.config) : m.config;\n const identityRole = cfg?.identity?.role;\n if (identityRole) roleMap.set(m.id, identityRole);\n }\n agents = agents.map((a: any) => {\n const engineRole = roleMap.get(a.id);\n return engineRole ? { ...a, role: engineRole } : a;\n });\n }\n } catch {}\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI (GPT-5 family — 1M context)\n { provider: 'openai', modelId: 'gpt-5', displayName: 'GPT-5', inputCostPerMillion: 1.25, outputCostPerMillion: 10, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-5-mini', displayName: 'GPT-5 Mini', inputCostPerMillion: 0.25, outputCostPerMillion: 2, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-5-nano', displayName: 'GPT-5 Nano', inputCostPerMillion: 0.05, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (1M–2M context)\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'google', modelId: 'gemini-3-flash', displayName: 'Gemini 3 Flash', inputCostPerMillion: 0.3, outputCostPerMillion: 2.5, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n // DeepSeek (V3.2 — 128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3.2)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4-1', displayName: 'Grok 4.1', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-1-fast', displayName: 'Grok 4.1 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0.01`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0.01`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "8df8ec4871fea132e9287972f8e944eaa268e555": {"t": 1785765595.277624, "n": 274715, "h": "31900bdc7060f05c", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // Two credential shapes:\n // - Standard API key (sk-ant-api…) → x-api-key header.\n // - OAuth access token (sk-ant-oat…) → Authorization: Bearer +\n // the oauth/claude-code beta headers. These come from a Claude\n // subscription / Claude Code and are REJECTED (401) by the\n // x-api-key header, so probing with x-api-key falsely flagged a\n // perfectly valid token as \"Invalid API key\". Mirror exactly\n // what the runtime's callAnthropic() does so validation agrees\n // with actual call-time behaviour.\n var isOAuthToken = apiKey.includes('sk-ant-oat');\n var anthropicHeaders: Record = isOAuthToken\n ? {\n 'authorization': 'Bearer ' + apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',\n 'user-agent': 'claude-cli/1.0.0 (external, cli)',\n 'x-app': 'cli',\n 'content-type': 'application/json',\n }\n : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' };\n var anthropicBody: Record = { model: 'claude-haiku-4-5-20251001', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] };\n // OAuth tokens are gated on the Claude Code identity as the first\n // system block (same requirement as the runtime) — include it so\n // the probe matches real call-time behaviour.\n if (isOAuthToken) {\n anthropicBody.system = [{ type: 'text', text: \"You are Claude Code, Anthropic's official CLI for Claude.\" }];\n }\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: anthropicHeaders,\n body: JSON.stringify(anthropicBody),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid creds, bad request) = creds work; 401/403 = bad creds\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n // Merge engine identity.role (source of truth) into admin agent records\n try {\n const edb = db.getEngineDB?.();\n if (edb) {\n const managed = await edb.all(`SELECT id, config FROM managed_agents`) || [];\n const roleMap = new Map();\n for (const m of managed as any[]) {\n const cfg = typeof m.config === 'string' ? JSON.parse(m.config) : m.config;\n const identityRole = cfg?.identity?.role;\n if (identityRole) roleMap.set(m.id, identityRole);\n }\n agents = agents.map((a: any) => {\n const engineRole = roleMap.get(a.id);\n return engineRole ? { ...a, role: engineRole } : a;\n });\n }\n } catch {}\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI (GPT-5 family — 1M context)\n { provider: 'openai', modelId: 'gpt-5', displayName: 'GPT-5', inputCostPerMillion: 1.25, outputCostPerMillion: 10, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-5-mini', displayName: 'GPT-5 Mini', inputCostPerMillion: 0.25, outputCostPerMillion: 2, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-5-nano', displayName: 'GPT-5 Nano', inputCostPerMillion: 0.05, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (1M–2M context)\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'google', modelId: 'gemini-3-flash', displayName: 'Gemini 3 Flash', inputCostPerMillion: 0.3, outputCostPerMillion: 2.5, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n // DeepSeek (V3.2 — 128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3.2)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4-1', displayName: 'Grok 4.1', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-1-fast', displayName: 'Grok 4.1 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0.01`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0.01`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "291c114272b0c4e2aedd031753613220dadfbe16": {"t": 1785765597.3476846, "n": 273903, "h": "efb5ddfa51eda462", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // Two credential shapes:\n // - Standard API key (sk-ant-api…) → x-api-key header.\n // - OAuth access token (sk-ant-oat…) → Authorization: Bearer +\n // the oauth/claude-code beta headers. These come from a Claude\n // subscription / Claude Code and are REJECTED (401) by the\n // x-api-key header, so probing with x-api-key falsely flagged a\n // perfectly valid token as \"Invalid API key\". Mirror exactly\n // what the runtime's callAnthropic() does so validation agrees\n // with actual call-time behaviour.\n var isOAuthToken = apiKey.includes('sk-ant-oat');\n var anthropicHeaders: Record = isOAuthToken\n ? {\n 'authorization': 'Bearer ' + apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',\n 'user-agent': 'claude-cli/1.0.0 (external, cli)',\n 'x-app': 'cli',\n 'content-type': 'application/json',\n }\n : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' };\n var anthropicBody: Record = { model: 'claude-haiku-4-5-20251001', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] };\n // OAuth tokens are gated on the Claude Code identity as the first\n // system block (same requirement as the runtime) — include it so\n // the probe matches real call-time behaviour.\n if (isOAuthToken) {\n anthropicBody.system = [{ type: 'text', text: \"You are Claude Code, Anthropic's official CLI for Claude.\" }];\n }\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: anthropicHeaders,\n body: JSON.stringify(anthropicBody),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid creds, bad request) = creds work; 401/403 = bad creds\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n // Merge engine identity.role (source of truth) into admin agent records\n try {\n const edb = db.getEngineDB?.();\n if (edb) {\n const managed = await edb.all(`SELECT id, config FROM managed_agents`) || [];\n const roleMap = new Map();\n for (const m of managed as any[]) {\n const cfg = typeof m.config === 'string' ? JSON.parse(m.config) : m.config;\n const identityRole = cfg?.identity?.role;\n if (identityRole) roleMap.set(m.id, identityRole);\n }\n agents = agents.map((a: any) => {\n const engineRole = roleMap.get(a.id);\n return engineRole ? { ...a, role: engineRole } : a;\n });\n }\n } catch {}\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0.01`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0.01`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "96d89de754564a63967aaa66361bd4ebfc695d09": {"t": 1785765599.529771, "n": 273477, "h": "578e63db1a02c0a8", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // Two credential shapes:\n // - Standard API key (sk-ant-api…) → x-api-key header.\n // - OAuth access token (sk-ant-oat…) → Authorization: Bearer +\n // the oauth/claude-code beta headers. These come from a Claude\n // subscription / Claude Code and are REJECTED (401) by the\n // x-api-key header, so probing with x-api-key falsely flagged a\n // perfectly valid token as \"Invalid API key\". Mirror exactly\n // what the runtime's callAnthropic() does so validation agrees\n // with actual call-time behaviour.\n var isOAuthToken = apiKey.includes('sk-ant-oat');\n var anthropicHeaders: Record = isOAuthToken\n ? {\n 'authorization': 'Bearer ' + apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',\n 'user-agent': 'claude-cli/1.0.0 (external, cli)',\n 'x-app': 'cli',\n 'content-type': 'application/json',\n }\n : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' };\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: anthropicHeaders,\n body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid creds, bad request) = creds work; 401/403 = bad creds\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n // Merge engine identity.role (source of truth) into admin agent records\n try {\n const edb = db.getEngineDB?.();\n if (edb) {\n const managed = await edb.all(`SELECT id, config FROM managed_agents`) || [];\n const roleMap = new Map();\n for (const m of managed as any[]) {\n const cfg = typeof m.config === 'string' ? JSON.parse(m.config) : m.config;\n const identityRole = cfg?.identity?.role;\n if (identityRole) roleMap.set(m.id, identityRole);\n }\n agents = agents.map((a: any) => {\n const engineRole = roleMap.get(a.id);\n return engineRole ? { ...a, role: engineRole } : a;\n });\n }\n } catch {}\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0.01`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0.01`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "440d7a35c4e78eae047c905e41baf74f8e1b7d35": {"t": 1785765602.7408988, "n": 272465, "h": "80a25b3217b97a2d", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n // Merge engine identity.role (source of truth) into admin agent records\n try {\n const edb = db.getEngineDB?.();\n if (edb) {\n const managed = await edb.all(`SELECT id, config FROM managed_agents`) || [];\n const roleMap = new Map();\n for (const m of managed as any[]) {\n const cfg = typeof m.config === 'string' ? JSON.parse(m.config) : m.config;\n const identityRole = cfg?.identity?.role;\n if (identityRole) roleMap.set(m.id, identityRole);\n }\n agents = agents.map((a: any) => {\n const engineRole = roleMap.get(a.id);\n return engineRole ? { ...a, role: engineRole } : a;\n });\n }\n } catch {}\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0.01`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0.01`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) >= 0.01).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "7c2a351f83a697eaf2c34f241eabbd0055d61416": {"t": 1785765605.915441, "n": 272435, "h": "3213422ecab7e58b", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n // Merge engine identity.role (source of truth) into admin agent records\n try {\n const edb = db.getEngineDB?.();\n if (edb) {\n const managed = await edb.all(`SELECT id, config FROM managed_agents`) || [];\n const roleMap = new Map();\n for (const m of managed as any[]) {\n const cfg = typeof m.config === 'string' ? JSON.parse(m.config) : m.config;\n const identityRole = cfg?.identity?.role;\n if (identityRole) roleMap.set(m.id, identityRole);\n }\n agents = agents.map((a: any) => {\n const engineRole = roleMap.get(a.id);\n return engineRole ? { ...a, role: engineRole } : a;\n });\n }\n } catch {}\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "f14ac9e60ba4b5e71128f19d8400e25882d6324a": {"t": 1785765607.9637244, "n": 271716, "h": "fe33f25d3c10e419", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { registerDuplicateRoutes } from './agent-duplicate.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n // ─── Agent Duplication ──────────────────────────────────────\n try {\n registerDuplicateRoutes(api, {\n getAdminDb: () => db,\n getEngineDb: () => db.getEngineDB?.(),\n getLifecycle: () => { try { const r = require('../engine/routes.js'); return r.getLifecycle?.(); } catch { return null; } },\n getPermissions: () => { try { const r = require('../engine/routes.js'); return r.getPermissions?.(); } catch { return null; } },\n requireRole,\n });\n } catch (e: any) { console.warn('[admin] Agent duplicate routes failed to register:', e.message); }\n\n return api;\n}\n"}, "41a179b1ce6c8ea52b5b8db30c15e5d8defabf11": {"t": 1785765610.5399985, "n": 271074, "h": "37910db7c1fcae62", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "30dd2472c3c209e8c6e38dc4d2c1f42baf4bb133": {"t": 1785765612.4421024, "n": 271051, "h": "73cdfb407b7f5250", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent (includes both active and archived trades)\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '100');\n // Query active trade log\n const active = await edb()?.all(`SELECT *, 'active' as source FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n // Also query archive for filled trades\n let archived: any[] = [];\n try {\n archived = await edb()?.all(`SELECT *, 'archive' as source FROM poly_trade_log_archive WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n } catch {} // archive table may not exist\n // Merge and sort by date, dedup by id\n const seen = new Set();\n const merged = [...active, ...archived].filter((r: any) => {\n if (seen.has(r.id)) return false;\n seen.add(r.id);\n return true;\n }).sort((a: any, b: any) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()).slice(0, limit);\n return c.json({ trades: merged });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "f4bf55ac69b1716c6389a5bdd0530faf62b2e625": {"t": 1785765615.3424065, "n": 270272, "h": "4ba67dcc0ccc4a1b", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab (with pagination and filters)\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '20');\n const side = c.req.query('side') || '';\n const status = c.req.query('status') || '';\n const search = c.req.query('search') || '';\n const offset = (page - 1) * pageSize;\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });\n\n try {\n let where = `agent_id = ?`;\n const params: any[] = [agentId];\n if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }\n if (status) { where += ` AND status = ?`; params.push(status); }\n if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }\n\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;\n const total = countRow?.cnt || 0;\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];\n return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });\n } catch {\n return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Only archive trades that are truly closed:\n // 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed\n // 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)\n // Do NOT archive filled trades for positions we still hold!\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log\n WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "7df274648b162ec74daf373e3b6499b3bf2d94ff": {"t": 1785765618.3956928, "n": 269361, "h": "d96a6a9bbb0fcd2c", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Get archived data for a specific tab\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const e = edb();\n const limit = parseInt(c.req.query('limit') || '100');\n\n let tableName = '';\n if (tab === 'trades') tableName = 'poly_trade_log_archive';\n else if (tab === 'exits') tableName = 'poly_exit_rules_archive';\n else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';\n else if (tab === 'events') tableName = 'poly_watcher_events_archive';\n else return c.json({ rows: [], total: 0, message: 'Unknown archive tab: ' + tab });\n\n // Check if archive table exists\n try {\n const rows = await e?.all(`SELECT * FROM ${tableName} WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE agent_id = ?`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || rows.length });\n } catch {\n // Archive table doesn't exist yet — no archived data\n return c.json({ rows: [], total: 0, message: 'No archived data yet. Use the Archive action to move old data here.' });\n }\n } catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }\n });\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "8afe7ba4e354771f94c9c815cdef0285d729a1ff": {"t": 1785765621.1612918, "n": 267968, "h": "c7dac33b17b1da42", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent (includes approval-pending AND placed-but-unfilled orders)\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Approval-mode pending trades\n const pendingRows = await edb()?.all(`SELECT *, 'approval' as source FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n // Auto-placed but not yet filled orders from trade log\n const placedRows = await edb()?.all(`SELECT *, 'placed' as source FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: [...pendingRows, ...placedRows] });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? AND status != 'placed' ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log — only use FILLED orders for position calculation\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'filled' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "ff4d49246297c973499b74554db9aa9a6d018409": {"t": 1785765623.4756155, "n": 267537, "h": "2441086b0e6bf6b4", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // SAFETY: Check if wallet already exists — require explicit confirmation to overwrite\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address && existing.funder_address !== address && !body.confirm_overwrite) {\n return c.json({\n error: 'EXISTING_WALLET',\n existing_address: existing.funder_address,\n new_address: address,\n message: `This agent already has a wallet at ${existing.funder_address}. Importing a new key will PERMANENTLY replace it. Export and back up the existing private key first. Send confirm_overwrite: true to proceed.`,\n }, 409);\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address, replaced: existing?.funder_address || null }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "057cb63d3edd90de2aa7a61e0fc42c95aa673dda": {"t": 1785765625.4047027, "n": 266767, "h": "3a134a551250dbd1", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const mismatch = signerAddress && signerAddress !== row.funder_address;\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n mismatch: !!mismatch,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n // Decrypt the private key\n const pk: string = (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })();\n\n // Derive the REAL address from the decrypted key\n let derivedAddress: string | null = null;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet: W } = req('@ethersproject/wallet');\n derivedAddress = new W(pk).address;\n } catch {\n try { const { Wallet: W } = await import('ethers' as any); derivedAddress = new W(pk).address; } catch {}\n }\n\n const mismatch = derivedAddress && derivedAddress !== row.funder_address;\n return c.json({\n address: derivedAddress || row.funder_address,\n storedFunderAddress: row.funder_address,\n privateKey: pk,\n mismatch: !!mismatch,\n warning: mismatch\n ? `CRITICAL: The private key derives address ${derivedAddress} but the stored funder address is ${row.funder_address}. These do NOT match! The key may have been corrupted by a vault key change. Check your VAULT_KEY environment variable.`\n : 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "109f3d52637dc37c369c267fc07414261813e6c8": {"t": 1785765627.4097557, "n": 265543, "h": "99fc8d4c1277b33d", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, private_key_encrypted, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ wallet: null });\n\n // Derive signer address from private key so UI can show the real trading address\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n return c.json({ wallet: {\n address: row.funder_address,\n signerAddress: signerAddress || row.funder_address,\n signatureType: row.signature_type,\n connected: true,\n createdAt: row.created_at,\n }});\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, show both funder and signer addresses\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the signer address from the private key\n let signerAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n signerAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); signerAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n const isSameAddress = signerAddress === row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n wallet: {\n funderAddress: row.funder_address,\n signerAddress,\n isSameAddress,\n updatedAt: row.updated_at,\n },\n message: flushed\n ? 'Cache flushed. Agent will reload from DB on next use.'\n : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "0ba55eb0b85f3f47ce339dbba32e2b096706ec05": {"t": 1785765629.6642044, "n": 264748, "h": "8a6b3a1fd21a1335", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, derive true address from private key, fix DB if mismatched\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n const row = await edb()?.get(`SELECT funder_address, private_key_encrypted, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ status: 'ok', flushed, wallet: null, message: 'No wallet found in database.' });\n\n // Derive the real address from the private key\n let derivedAddress: string | null = null;\n if (row.private_key_encrypted) {\n try {\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any; try { vault = new SecureVault(); } catch {}\n const pk = vault ? vault.decrypt(row.private_key_encrypted) : row.private_key_encrypted;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n derivedAddress = new Wallet(pk).address;\n } catch {\n try { const { Wallet } = await import('ethers' as any); derivedAddress = new Wallet(pk).address; } catch {}\n }\n } catch {}\n }\n\n // Fix DB if funder_address doesn't match derived address\n let fixed = false;\n if (derivedAddress && row.funder_address !== derivedAddress) {\n await edb()?.run(`UPDATE poly_wallet_credentials SET funder_address = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`, [derivedAddress, agentId]);\n fixed = true;\n }\n\n const address = derivedAddress || row.funder_address;\n return c.json({\n status: 'ok',\n flushed,\n fixed,\n wallet: { address, updatedAt: row.updated_at },\n message: fixed\n ? `Address corrected: ${row.funder_address} → ${derivedAddress}. Cache flushed.`\n : flushed ? 'Cache flushed. Agent will reload from DB on next use.' : 'Wallet is in sync.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "5598dc2d84dc3162550a99c82b1111f4f3314a27": {"t": 1785765632.1276464, "n": 263203, "h": "f3d1ed5096c0fb1a", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, force reload from DB\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n // Verify wallet exists in DB\n const row = await edb()?.get(`SELECT funder_address, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n return c.json({\n status: 'ok',\n flushed,\n wallet: row ? { address: row.funder_address, updatedAt: row.updated_at } : null,\n message: flushed ? 'In-memory wallet cache flushed. Agent will reload from DB on next use.' : 'No cached wallet found. Agent will load from DB on next use.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, whitelist_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, wl.id, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "b2d58bb246177aff43a9d9a166bb0c02b04d7880": {"t": 1785765634.2537508, "n": 263179, "h": "30673d1ea5da05f8", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, force reload from DB\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n // Verify wallet exists in DB\n const row = await edb()?.get(`SELECT funder_address, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n return c.json({\n status: 'ok',\n flushed,\n wallet: row ? { address: row.funder_address, updatedAt: row.updated_at } : null,\n message: flushed ? 'In-memory wallet cache flushed. Agent will reload from DB on next use.' : 'No cached wallet found. Agent will load from DB on next use.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit, cooling_until FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, to_address]) as any;\n if (wl && wl.cooling_until && new Date(wl.cooling_until) > new Date()) return c.json({ error: 'Address is still in cooling period until ' + wl.cooling_until }, 403);\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const oneDayAgo = new Date(Date.now() - 86400000).toISOString();\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > ?`, [agentId, to_address, oneDayAgo]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + 4 * 3600000).toISOString();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, ?)`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '', expiresAt]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "5d38d537e01b8a1c40c8fbbc3eef635e17f8ed07": {"t": 1785765636.4551353, "n": 262913, "h": "279862bd11273814", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB, flushClobClient } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n flushClobClient(agentId);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Sync wallet — flush in-memory cache, force reload from DB\n api.post('/polymarket/:agentId/wallet/sync', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const flushed = flushClobClient(agentId);\n // Verify wallet exists in DB\n const row = await edb()?.get(`SELECT funder_address, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n return c.json({\n status: 'ok',\n flushed,\n wallet: row ? { address: row.funder_address, updatedAt: row.updated_at } : null,\n message: flushed ? 'In-memory wallet cache flushed. Agent will reload from DB on next use.' : 'No cached wallet found. Agent will load from DB on next use.',\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "782bbc0c493bc20e01b8fd52e21871cdd66c16f6": {"t": 1785765638.8906975, "n": 261974, "h": "1f35e81db6f712c8", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "2610467196b49326247c068d60791dbf2e502a06": {"t": 1785765641.10248, "n": 261973, "h": "40c5808106a81b41", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n // Prefer daily_pnl_usd (absolute $) over daily_pnl_pct (percentage) — the scorecard modal sets USD targets\n const dailyGoal = await edb()?.get(`SELECT target_value, type FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY CASE type WHEN 'daily_pnl_usd' THEN 0 ELSE 1 END LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "8d651d64d6a3d5afc9292644c92e725e65c8255a": {"t": 1785765643.4098458, "n": 261813, "h": "65bb1d166c1c1de4", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Positions from Polymarket Data API (sizeThreshold=0 to include resolved) ──\n let allPositions: any[] = [];\n let openPositions: any[] = [];\n let unrealizedPnl = 0;\n let realizedPnl = 0;\n let deployed = 0;\n let winsToday = 0;\n let lossesToday = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}&sizeThreshold=0`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n allPositions = data;\n for (const pos of data) {\n const pnl = parseFloat(pos.cashPnl ?? pos.pnl ?? '0');\n const size = parseFloat(pos.size ?? '0');\n const entryPrice = parseFloat(pos.avgPrice ?? pos.avg_price ?? '0');\n const currentPrice = parseFloat(pos.curPrice ?? pos.current_price ?? '0');\n if (isNaN(pnl)) continue;\n\n if (size <= 0 || pos.resolved || pos.closed) {\n // Closed/resolved position — cashPnl is realized P&L\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n } else {\n // Open position — calculate unrealized from price difference\n openPositions.push(pos);\n const unrealPnl = currentPrice > 0 && entryPrice > 0\n ? (currentPrice - entryPrice) * size\n : pnl;\n unrealizedPnl += unrealPnl;\n deployed += size * entryPrice;\n }\n }\n }\n } catch {}\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "c6f304088dfea1c54ec010c276eaec243087a7d7": {"t": 1785765645.1801803, "n": 262042, "h": "bbd56d364b98d805", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet address\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n const address = walletRow?.funder_address;\n\n // Wallet balance — multi-RPC fallback\n let usdcBalance = 0;\n if (address) {\n const RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com'];\n const addrHex = address.slice(2).toLowerCase();\n const callData = '0x70a08231000000000000000000000000' + addrHex;\n for (const rpc of RPCS) {\n try {\n const r = await fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E_SHARED, data: callData }, 'latest'] }),\n signal: AbortSignal.timeout(4000),\n }).then(r => r.json());\n if (r?.result && !r.error) { usdcBalance = Number(BigInt(r.result)) / 1e6; break; }\n } catch {}\n }\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // ── Real positions from Polymarket Data API ──\n let livePositions: any[] = [];\n let unrealizedPnl = 0;\n let deployed = 0;\n if (address) {\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`, { signal: AbortSignal.timeout(8000) });\n const data = await resp.json();\n if (Array.isArray(data)) {\n livePositions = data.filter((p: any) => parseFloat(p.size) > 0);\n unrealizedPnl = livePositions.reduce((s: number, p: any) => s + (parseFloat(p.cashPnl) || 0), 0);\n deployed = livePositions.reduce((s: number, p: any) => s + (parseFloat(p.initialValue) || 0), 0);\n }\n } catch {}\n }\n\n // ── Realized P&L from trade log (SELL trades today) ──\n const sellTrades = await edb()?.all(\n `SELECT price, size, token_id FROM poly_trade_log WHERE agent_id = ? AND side = 'SELL' AND created_at >= ? AND status = 'placed' AND clob_order_id IS NOT NULL`,\n [agentId, todayStart]\n ).catch(() => []) as any[] || [];\n // Match sells to buys for realized P&L\n let realizedPnl = 0;\n let winsToday = 0;\n let lossesToday = 0;\n for (const sell of sellTrades) {\n // Find matching buy for this token\n const buy = await edb()?.get(\n `SELECT price FROM poly_trade_log WHERE agent_id = ? AND token_id = ? AND side = 'BUY' AND status = 'placed' AND clob_order_id IS NOT NULL ORDER BY created_at DESC LIMIT 1`,\n [agentId, sell.token_id]\n ).catch(() => null) as any;\n const buyPrice = parseFloat(buy?.price) || 0;\n const sellPrice = parseFloat(sell.price) || 0;\n const sellSize = parseFloat(sell.size) || 0;\n const pnl = (sellPrice - buyPrice) * sellSize;\n realizedPnl += pnl;\n if (pnl > 0) winsToday++;\n else if (pnl < 0) lossesToday++;\n }\n\n // Trade count from counter (most accurate) or trade log\n const tradesToday = counter?.trade_count || 0;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = realizedPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const winRate = (winsToday + lossesToday) > 0 ? (winsToday / (winsToday + lossesToday)) * 100 : 0;\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +realizedPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: livePositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +deployed.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "9c28532f0a9d0c0b27bfd438cfc429ab237bad0e": {"t": 1785765647.2302654, "n": 260554, "h": "9b3072192add0bb2", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'];\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n\n // Try each RPC until we get a valid balance response (not just a ping)\n let usdceRes: any = null, usdcNativeRes: any = null, maticRes: any = null;\n let rpcSuccess = false;\n for (const rpc of POLYGON_RPCS) {\n try {\n const [r1, r2, r3] = await Promise.all([\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n fetch(rpc, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] }),\n signal: AbortSignal.timeout(5000),\n }).then(r => r.json()).catch(() => null),\n ]);\n // Check if at least USDC.e returned a valid result (primary balance)\n if (r1?.result && !r1.error) {\n usdceRes = r1; usdcNativeRes = r2; maticRes = r3;\n rpcSuccess = true;\n break;\n }\n } catch {}\n }\n\n const cached = balanceCache.get(address);\n\n // Start with cached values as fallback (don't default to 0)\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (!rpcSuccess) {\n console.log(`[wallet-balance] All RPCs failed for ${address.slice(0, 10)}... — using ${cached ? 'cached' : 'zero'} values`);\n }\n\n // Only cache when RPC actually returned data — never overwrite good cache with zeros from failed RPCs\n if (rpcSuccess) {\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n }\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "5be8b8a655dc5497dc35f37bf297d28d679eece1": {"t": 1785765650.1541867, "n": 260208, "h": "72ab8503898981e9", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = (e as any).execute ? e : Object.assign({}, e, {\n execute: (e as any).run || (e as any).execute,\n query: (e as any).all || (e as any).query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n // ── Redeem winning positions — direct on-chain, no LLM needed ──\n api.post('/polymarket/:agentId/wallet/redeem', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n const conditionId = body.condition_id;\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n // Fetch redeemable positions from Polymarket Data API\n const posRes = await fetch(`https://data-api.polymarket.com/positions?user=${creds.funder_address}&sizeThreshold=0`);\n const positions = await posRes.json();\n const redeemable = (positions as any[]).filter((pos: any) => pos.redeemable === true);\n if (!redeemable.length) return c.json({ ok: true, status: 'nothing_to_redeem', message: 'No redeemable positions found.' });\n\n // Filter by condition_id if specified, otherwise redeem all\n const toRedeem = conditionId\n ? redeemable.filter((pos: any) => pos.conditionId === conditionId)\n : redeemable;\n if (!toRedeem.length) return c.json({ error: `No redeemable position found for condition ${conditionId}` }, 404);\n\n // Connect to Polygon\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';\n const CTF_ABI = ['function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] calldata indexSets) external'];\n\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n const ctf = new Contract(CTF, CTF_ABI, wallet);\n const parentCollectionId = '0x0000000000000000000000000000000000000000000000000000000000000000';\n\n const results: any[] = [];\n for (const pos of toRedeem) {\n try {\n const indexSets = pos.negativeRisk ? [1] : [1, 2];\n const tx = await ctf.redeemPositions(USDC_E_SHARED, parentCollectionId, pos.conditionId, indexSets, { gasLimit: 300000 });\n const receipt = await tx.wait();\n\n // Update trade log (best effort)\n try {\n await edb()?.run(`UPDATE poly_trade_log SET status = 'redeemed', pnl = ? WHERE token_id = ? AND agent_id = ? AND status != 'redeemed'`,\n [pos.cashPnl || 0, pos.asset, agentId]);\n } catch {}\n\n results.push({ title: pos.title, outcome: pos.outcome, conditionId: pos.conditionId, shares: pos.size, value: pos.currentValue, profit: pos.cashPnl, txHash: receipt.hash, status: 'redeemed' });\n } catch (e: any) {\n results.push({ title: pos.title, conditionId: pos.conditionId, status: 'failed', error: e.message });\n }\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.redeem', resourceType: 'agent', resourceId: agentId, details: { redeemed: results.filter(r => r.status === 'redeemed').length, failed: results.filter(r => r.status === 'failed').length }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n redeemed: results.filter(r => r.status === 'redeemed').length,\n failed: results.filter(r => r.status === 'failed').length,\n total_value: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.value || 0), 0),\n total_profit: results.filter(r => r.status === 'redeemed').reduce((s, r) => s + (r.profit || 0), 0),\n details: results,\n });\n } catch (e: any) { return c.json({ error: `Redeem failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (usdceBalance === 0 && maticBalance > 0) {\n console.log(`[wallet-balance] USDC.e=0 for ${address.slice(0, 10)}... (RPC result: ${JSON.stringify(usdceRes?.result?.slice?.(0, 20) ?? usdceRes?.error ?? 'null')})`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n const isResolved = p.redeemable || curPrice === 1 || curPrice === 0;\n if (isResolved) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n conditionId: p.conditionId || '',\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: isResolved ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n isWon: isResolved && curPrice >= 0.99,\n isLost: isResolved && curPrice <= 0.01,\n };\n }).filter((p: any) => {\n // Auto-hide lost positions after 3 days (non-redeemable, resolved to 0)\n if (p.isLost && !p.redeemable && p.endDate) {\n const endTime = new Date(p.endDate).getTime();\n if (!isNaN(endTime) && Date.now() - endTime > THREE_DAYS_MS) return false;\n }\n return true;\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n conditionId: p.conditionId || '',\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n isWon: p.isWon || false,\n isLost: p.isLost || false,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "3cfac37dfc46fed51d754a86e80551102db2c8d2": {"t": 1785765652.1101882, "n": 255035, "h": "946c009ecccba79a", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = e.execute ? e : Object.assign({}, e, {\n execute: e.run || e.execute,\n query: e.all || e.query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n const safeBigInt = (hex: string): bigint => { try { return hex && hex.length > 2 ? BigInt(hex) : 0n; } catch { return 0n; } };\n if (usdceRes?.result && !usdceRes.error) {\n usdceBalance = Number(safeBigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && !usdcNativeRes.error) {\n usdcNativeBalance = Number(safeBigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && !maticRes.error) {\n maticBalance = Number(safeBigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Log RPC results for debugging balance issues\n if (usdceBalance === 0 && maticBalance > 0) {\n console.log(`[wallet-balance] USDC.e=0 for ${address.slice(0, 10)}... (RPC result: ${JSON.stringify(usdceRes?.result?.slice?.(0, 20) ?? usdceRes?.error ?? 'null')})`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (Number(pp.total_invested) || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (Number(pp.open_count) || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n if (p.redeemable || curPrice === 1 || curPrice === 0) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: p.redeemable ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n };\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "ec3abdfa322c0f4feb92546c03ebff2506a9809c": {"t": 1785765654.1172829, "n": 254668, "h": "f30221cdbbb562a7", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = e.execute ? e : Object.assign({}, e, {\n execute: e.run || e.execute,\n query: e.all || e.query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct',\n 'proactive_interval_mins', 'proactive_max_daily'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.drpc.org', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && usdceRes.result !== '0x' && !usdceRes.error) {\n usdceBalance = Number(BigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && usdcNativeRes.result !== '0x' && !usdcNativeRes.error) {\n usdcNativeBalance = Number(BigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && maticRes.result !== '0x' && !maticRes.error) {\n maticBalance = Number(BigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (pp.total_invested || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (pp.open_count || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n if (p.redeemable || curPrice === 1 || curPrice === 0) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: p.redeemable ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n };\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "7a31ef220122dec2b6a4cf3a0453a5f7a188c5e3": {"t": 1785765656.9260674, "n": 254610, "h": "73168821a6c37d90", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = e.execute ? e : Object.assign({}, e, {\n execute: e.run || e.execute,\n query: e.all || e.query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.drpc.org', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && usdceRes.result !== '0x' && !usdceRes.error) {\n usdceBalance = Number(BigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && usdcNativeRes.result !== '0x' && !usdcNativeRes.error) {\n usdcNativeBalance = Number(BigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && maticRes.result !== '0x' && !maticRes.error) {\n maticBalance = Number(BigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (pp.total_invested || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (pp.open_count || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n if (p.redeemable || curPrice === 1 || curPrice === 0) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: p.redeemable ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n };\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n // Note: /markets endpoint ignores search param — only use /events for keyword search\n const [result, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n const directMarkets: any[] = [];\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "335264e27e72a7d59048d688d6a7405f94caf43a": {"t": 1785765659.3349924, "n": 254647, "h": "6b46a92b2fcdfbae", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = e.execute ? e : Object.assign({}, e, {\n execute: e.run || e.execute,\n query: e.all || e.query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.drpc.org', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && usdceRes.result !== '0x' && !usdceRes.error) {\n usdceBalance = Number(BigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && usdcNativeRes.result !== '0x' && !usdcNativeRes.error) {\n usdcNativeBalance = Number(BigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && maticRes.result !== '0x' && !maticRes.error) {\n maticBalance = Number(BigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (pp.total_invested || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (pp.open_count || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n if (p.redeemable || curPrice === 1 || curPrice === 0) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: p.redeemable ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n };\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n const [result, directMarkets, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/markets?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n const e = edb();\n if (action === 'pause') {\n // 1. Mark daily counter as paused\n await e?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n // 2. Also set proactive pause (watcher checks this table)\n try {\n await e?.run(`CREATE TABLE IF NOT EXISTS poly_proactive_pause (agent_id TEXT PRIMARY KEY, paused_at TEXT NOT NULL, reason TEXT)`);\n await e?.run(`INSERT OR REPLACE INTO poly_proactive_pause (agent_id, paused_at, reason) VALUES (?, ?, ?)`,\n [agentId, new Date().toISOString(), reason || 'Dashboard paused']);\n } catch {}\n } else {\n // 1. Clear daily counter pause\n await e?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n // 2. Clear proactive pause (so watcher can wake agent again)\n try {\n await e?.run(`DELETE FROM poly_proactive_pause WHERE agent_id = ?`, [agentId]);\n } catch {}\n // 3. Wake the agent to resume trading\n try {\n const agentRow = await e?.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n const agentConfig = typeof agentRow?.config === 'string' ? JSON.parse(agentRow.config) : agentRow?.config;\n const dep = agentConfig?.deployment;\n const port = dep?.port || dep?.config?.local?.port || 3101;\n const secret = process.env.AGENT_RUNTIME_SECRET || process.env.JWT_SECRET || '';\n // Determine best channel for wake message\n const messaging = agentConfig?.messagingChannels || {};\n const tgChatId = messaging.telegram?.chatId || messaging.telegram?.trustedChatIds?.[0] || agentConfig?.managerIdentity?.telegramId;\n const wakeSource = tgChatId ? 'telegram' : 'system';\n const wakeSenderId = tgChatId || 'dashboard@system';\n const wakeSpaceId = tgChatId || 'dashboard_resume';\n const resp = await fetch(`http://127.0.0.1:${port}/api/runtime/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${secret}` },\n body: JSON.stringify({\n source: wakeSource,\n senderName: 'Manager',\n senderEmail: wakeSenderId,\n spaceName: 'DM',\n spaceId: wakeSpaceId,\n threadId: '',\n isDM: true,\n messageText: '[TRADING RESUMED] Your trading has been resumed from the dashboard. Check your current positions and look for new opportunities.',\n isManager: true,\n }),\n signal: AbortSignal.timeout(10000),\n });\n if (!resp.ok) console.warn(`[polymarket] Resume wake failed: ${resp.status}`);\n } catch (wakeErr: any) {\n console.warn(`[polymarket] Resume wake error: ${wakeErr.message}`);\n }\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "ebf953d307dfba0d01bd6c978deac7325ced61a4": {"t": 1785765661.8753936, "n": 252027, "h": "cfbae6da71aa92d9", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = e.execute ? e : Object.assign({}, e, {\n execute: e.run || e.execute,\n query: e.all || e.query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.drpc.org', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && usdceRes.result !== '0x' && !usdceRes.error) {\n usdceBalance = Number(BigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && usdcNativeRes.result !== '0x' && !usdcNativeRes.error) {\n usdcNativeBalance = Number(BigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && maticRes.result !== '0x' && !maticRes.error) {\n maticBalance = Number(BigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (pp.total_invested || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (pp.open_count || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n if (p.redeemable || curPrice === 1 || curPrice === 0) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: p.redeemable ? curPrice : undefined,\n endDate: p.endDate || p.expirationDate || '',\n };\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n endDate: p.endDate || '',\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n const [result, directMarkets, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/markets?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n if (action === 'pause') {\n await edb()?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n } else {\n await edb()?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}, "ee2eaf30bf399e6522f0392d8d6db5fde23a40e8": {"t": 1785765663.8941407, "n": 251917, "h": "e1895726665cf7ef", "c": "/**\n * Admin API Routes\n * \n * CRUD for agents, users, audit logs, rules, settings.\n * All routes are protected by auth middleware (applied in server.ts).\n * Input validation on all mutations. RBAC on sensitive operations.\n */\n\nimport { Hono } from 'hono';\nimport { configBus } from '../engine/config-bus.js';\nimport type { AppEnv } from '../types/hono-env.js';\nimport type { DatabaseAdapter } from '../db/adapter.js';\nimport { validate, requireRole, ValidationError, transportEncryptionMiddleware } from '../middleware/index.js';\nimport { PROVIDER_REGISTRY, type ProviderDef } from '../runtime/providers.js';\nimport { USDC_ADDRESS as USDC_E_SHARED } from '../polymarket-engines/shared.js';\n\n/**\n * Validate an API key by making a lightweight request to the provider.\n * Each provider has a different validation endpoint.\n */\nasync function validateProviderApiKey(\n providerId: string,\n apiKey: string,\n provider: ProviderDef,\n): Promise<{ ok: boolean; error?: string }> {\n const timeout = 10_000;\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), timeout);\n\n try {\n let resp: Response;\n\n switch (providerId) {\n case 'anthropic': {\n // POST /v1/messages with a tiny request — Anthropic returns 401 for bad keys\n resp = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'claude-haiku-4-20250414', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),\n signal: ctrl.signal,\n });\n // 200 or 400 (valid key, bad request) = key works; 401/403 = bad key\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'openai': {\n // GET /v1/models — lightweight, just lists models\n resp = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'google': {\n // GET /v1beta/models — list Gemini models\n resp = await fetch('https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey, {\n signal: ctrl.signal,\n });\n if (resp.status === 400 || resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key (HTTP ' + resp.status + ')' };\n }\n return { ok: true };\n }\n\n case 'xai': {\n resp = await fetch('https://api.x.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'deepseek': {\n resp = await fetch('https://api.deepseek.com/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'mistral': {\n resp = await fetch('https://api.mistral.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'groq': {\n resp = await fetch('https://api.groq.com/openai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'together': {\n resp = await fetch('https://api.together.xyz/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'openrouter': {\n resp = await fetch('https://openrouter.ai/api/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'fireworks': {\n resp = await fetch('https://api.fireworks.ai/inference/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n case 'cerebras': {\n resp = await fetch('https://api.cerebras.ai/v1/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n }\n\n // Local providers (ollama, vllm, lmstudio, litellm) — skip validation\n case 'ollama':\n case 'vllm':\n case 'lmstudio':\n case 'litellm':\n return { ok: true };\n\n default: {\n // For unknown/custom providers, try GET /models with Bearer auth\n try {\n resp = await fetch(provider.baseUrl + '/models', {\n headers: { Authorization: 'Bearer ' + apiKey },\n signal: ctrl.signal,\n });\n if (resp.status === 401 || resp.status === 403) {\n return { ok: false, error: 'Invalid API key' };\n }\n return { ok: true };\n } catch {\n // Can't reach — skip validation for custom providers\n return { ok: true };\n }\n }\n }\n } catch (e: any) {\n if (e.name === 'AbortError') {\n return { ok: false, error: 'Validation timed out — provider not reachable' };\n }\n return { ok: false, error: e.message || 'Connection failed' };\n } finally {\n clearTimeout(timer);\n }\n}\nimport { deployToFly, getAppStatus, destroyApp, type FlyConfig, type AppConfig } from '../deploy/fly.js';\nimport { SecureVault } from '../engine/vault.js';\nimport { getWatcherEngineStatus, controlWatcherEngine } from '../agent-tools/tools/polymarket-watcher.js';\nimport { importSDK, getProxyState, loadProxyConfig, saveProxyConfig, startProxy, stopProxy, deployProxyToVPS, autoConnectProxy, initPolymarketDB } from '../agent-tools/tools/polymarket-runtime.js';\nimport { executeOrder } from '../agent-tools/tools/polymarket.js';\n\n// Shared vault instance for encrypting/decrypting provider API keys\nconst vault = new SecureVault();\n\nexport function createAdminRoutes(db: DatabaseAdapter) {\n const api = new Hono();\n\n // Transport encryption middleware — decrypts incoming, encrypts outgoing\n api.use('*', transportEncryptionMiddleware());\n\n // Wrapper: updateSettings + auto-emit config change events\n const updateSettingsAndEmit = async (updates: any) => {\n const result = await db.updateSettings(updates);\n configBus.emitSettings(Object.keys(updates));\n return result;\n };\n\n // ─── Dashboard Stats ────────────────────────────────\n\n api.get('/stats', async (c) => {\n const clientOrgId = c.req.query('clientOrgId') || '';\n if (clientOrgId) {\n // Scoped stats for client org users\n const allAgents = await db.listAgents({ limit: 1000, offset: 0 });\n const orgAgents = allAgents.filter((a: any) => a.client_org_id === clientOrgId);\n const activeOrgAgents = orgAgents.filter((a: any) => a.status === 'active');\n // Count users in this client org\n let orgUsers = 0;\n try {\n const allUsers = await db.listUsers();\n orgUsers = allUsers.filter((u: any) => u.client_org_id === clientOrgId).length;\n } catch { orgUsers = 0; }\n // Count audit events for this org's agents\n let orgAudit = 0;\n try {\n const agentIds = orgAgents.map((a: any) => a.id);\n if (agentIds.length > 0) {\n const result = await (db as any).pool?.query?.(\n `SELECT COUNT(*) FROM audit_log WHERE org_id = $1`,\n [clientOrgId]\n );\n orgAudit = result?.rows?.[0]?.count ? parseInt(result.rows[0].count, 10) : 0;\n }\n } catch { orgAudit = 0; }\n return c.json({\n totalAgents: orgAgents.length,\n activeAgents: activeOrgAgents.length,\n totalUsers: orgUsers,\n totalEmails: 0,\n totalAuditEvents: orgAudit,\n });\n }\n const stats = await db.getStats();\n return c.json(stats);\n });\n\n // ─── Agents ─────────────────────────────────────────\n\n api.get('/agents', async (c) => {\n const status = c.req.query('status') as any;\n const clientOrgId = c.req.query('clientOrgId') || '';\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n let agents = await db.listAgents({ status, limit, offset });\n let total = await db.countAgents(status);\n // Filter by client org if requested\n if (clientOrgId) {\n agents = agents.filter((a: any) => a.client_org_id === clientOrgId);\n total = agents.length;\n }\n return c.json({ agents, total, limit, offset });\n });\n\n api.get('/agents/:id', async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n return c.json(agent);\n });\n\n api.post('/agents', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n ]);\n\n // Check for duplicate name\n const existing = await db.getAgentByName(body.name);\n if (existing) {\n return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const userId = c.get('userId') || 'system';\n const agent = await db.createAgent({ ...body, createdBy: userId });\n return c.json(agent, 201);\n });\n\n api.patch('/agents/:id', async (c) => {\n const id = c.req.param('id');\n const existing = await db.getAgent(id);\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 64 },\n { field: 'email', type: 'email' },\n { field: 'role', type: 'string', maxLength: 32 },\n { field: 'status', type: 'string', pattern: /^(active|archived|suspended)$/ },\n ]);\n\n // If renaming, check for conflicts\n if (body.name && body.name !== existing.name) {\n const conflict = await db.getAgentByName(body.name);\n if (conflict) return c.json({ error: 'Agent name already exists' }, 409);\n }\n\n const agent = await db.updateAgent(id, body);\n\n // Update billing_rate if provided\n if ('billingRate' in body || 'billing_rate' in body) {\n const rate = body.billingRate ?? body.billing_rate ?? 0;\n try {\n await (db as any).pool.query('UPDATE agents SET billing_rate = $1 WHERE id = $2', [rate, id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE agents SET billing_rate = ? WHERE id = ?').run(rate, id); } catch { /* ignore */ }\n }\n }\n\n configBus.emitAgentUpdate(id, Object.keys(body));\n return c.json(agent);\n });\n\n api.post('/agents/:id/archive', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status === 'archived') return c.json({ error: 'Agent already archived' }, 400);\n\n await db.archiveAgent(c.req.param('id'));\n return c.json({ ok: true, status: 'archived' });\n });\n\n api.post('/agents/:id/restore', async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n if (existing.status !== 'archived') return c.json({ error: 'Agent is not archived' }, 400);\n\n await db.updateAgent(c.req.param('id'), { status: 'active' } as any);\n return c.json({ ok: true, status: 'active' });\n });\n\n // Permanent delete — owner/admin only\n api.delete('/agents/:id', requireRole('admin'), async (c) => {\n const existing = await db.getAgent(c.req.param('id'));\n if (!existing) return c.json({ error: 'Agent not found' }, 404);\n\n await db.deleteAgent(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Agent Deployment ─────────────────────────────────\n\n api.post('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const body = await c.req.json().catch(() => ({}));\n const targetType = body.targetType || 'fly';\n const config = body.config || {};\n\n // Get deployment credentials\n const settings = await db.getSettings();\n const pricingConfig = (settings as any)?.modelPricingConfig || {};\n const _providerApiKeys = pricingConfig.providerApiKeys || {};\n\n if (targetType === 'fly') {\n // Get Fly.io API token from deploy credentials or config\n let flyToken = config.flyApiToken || process.env.FLY_API_TOKEN;\n if (!flyToken && body.credentialId) {\n // Look up stored credential\n try {\n const creds = await (db as any).query?.('SELECT config FROM deploy_credentials WHERE id = $1', [body.credentialId]);\n if (creds?.rows?.[0]?.config) {\n const credConfig = typeof creds.rows[0].config === 'string' ? JSON.parse(creds.rows[0].config) : creds.rows[0].config;\n flyToken = credConfig.apiToken;\n }\n } catch { /* ignore */ }\n }\n\n if (!flyToken) {\n return c.json({ error: 'Fly.io API token required. Add it in Settings → Deployments or pass flyApiToken in config.' }, 400);\n }\n\n const flyConfig: FlyConfig = {\n apiToken: flyToken,\n org: config.flyOrg || 'personal',\n image: config.image || 'node:22-slim',\n regions: config.regions || ['iad'],\n };\n\n const agentName = (agent as any).name || agentId;\n const appConfig: AppConfig = {\n subdomain: agentName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),\n dbType: 'postgres',\n dbConnectionString: process.env.DATABASE_URL || '',\n jwtSecret: process.env.JWT_SECRET || 'agent-' + agentId,\n smtpHost: (settings as any)?.smtpHost,\n smtpPort: (settings as any)?.smtpPort,\n smtpUser: (settings as any)?.smtpUser,\n smtpPass: (settings as any)?.smtpPass,\n memoryMb: config.memoryMb || 256,\n cpuKind: config.cpuKind || 'shared',\n cpus: config.cpus || 1,\n };\n\n try {\n const result = await deployToFly(appConfig, flyConfig);\n\n // Update agent record with deployment info (stored in metadata)\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: result.status === 'started' ? 'active' : 'error',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'fly',\n appName: result.appName,\n url: result.url,\n region: result.region,\n machineId: result.machineId,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: result.status,\n },\n },\n } as any);\n\n return c.json({\n success: result.status === 'started',\n deployment: result,\n });\n } catch (err: any) {\n return c.json({ error: 'Deployment failed: ' + err.message }, 500);\n }\n }\n\n if (targetType === 'local') {\n const existingAgent = await db.getAgent(agentId);\n const existingMeta = (existingAgent as any)?.metadata || {};\n await db.updateAgent(agentId, {\n status: 'active',\n metadata: {\n ...existingMeta,\n deployment: {\n target: 'local',\n url: `http://localhost:${3000 + Math.floor(Math.random() * 1000)}`,\n deployedAt: new Date().toISOString(),\n deployedBy: body.deployedBy || 'dashboard',\n status: 'started',\n },\n },\n } as any);\n return c.json({ success: true, deployment: { status: 'started', target: 'local' } });\n }\n\n return c.json({ error: 'Unsupported deploy target: ' + targetType + '. Supported: fly, docker, vps, local' }, 400);\n });\n\n // Get deployment status\n api.get('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ deployed: false });\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n const status = await getAppStatus(info.appName, { apiToken: flyToken });\n return c.json({ deployed: true, ...info, live: status });\n } catch { /* fall through */ }\n }\n }\n\n return c.json({ deployed: true, ...info });\n });\n\n // Destroy deployment\n api.delete('/agents/:id/deploy', requireRole('admin'), async (c) => {\n const agent = await db.getAgent(c.req.param('id'));\n if (!agent) return c.json({ error: 'Agent not found' }, 404);\n\n const meta = (agent as any).metadata || {};\n const info = meta.deployment;\n if (!info) return c.json({ error: 'Agent not deployed' }, 400);\n\n if (info.target === 'fly' && info.appName) {\n const flyToken = process.env.FLY_API_TOKEN;\n if (flyToken) {\n try {\n await destroyApp(info.appName, { apiToken: flyToken });\n } catch (err: any) {\n return c.json({ error: 'Failed to destroy: ' + err.message }, 500);\n }\n }\n }\n\n delete meta.deployment;\n await db.updateAgent(c.req.param('id'), { status: 'inactive', metadata: meta } as any);\n return c.json({ ok: true, message: 'Deployment destroyed' });\n });\n\n // ─── Users ──────────────────────────────────────────\n\n api.get('/users', requireRole('admin'), async (c) => {\n const limit = Math.min(parseInt(c.req.query('limit') || '50'), 200);\n const offset = Math.max(parseInt(c.req.query('offset') || '0'), 0);\n const users = await db.listUsers({ limit, offset });\n // Strip sensitive fields\n const safe = users.map(({ passwordHash, totpSecret, totpBackupCodes, ...u }) => u);\n return c.json({ users: safe, limit, offset });\n });\n\n api.post('/users', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email', required: true },\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', required: true, pattern: /^(owner|admin|member|viewer)$/ },\n { field: 'password', type: 'string', minLength: 8, maxLength: 128 },\n ]);\n\n // Check duplicate email\n const existing = await db.getUserByEmail(body.email);\n if (existing) return c.json({ error: 'Email already registered' }, 409);\n\n const user = await db.createUser(body);\n\n // Mark as must-reset-password (admin-created accounts)\n try {\n await (db as any).pool.query(\n 'UPDATE users SET must_reset_password = TRUE WHERE id = $1',\n [user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET must_reset_password = 1 WHERE id = ?').run(user.id);\n } catch { /* ignore */ }\n }\n\n // Set client org if provided\n if (body.clientOrgId) {\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [body.clientOrgId, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(body.clientOrgId, user.id); } catch { /* ignore */ }\n }\n }\n\n // Set initial permissions if provided\n if (body.permissions && body.permissions !== '*') {\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1 WHERE id = $2',\n [JSON.stringify(body.permissions), user.id]\n );\n } catch {\n try {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET permissions = ? WHERE id = ?').run(JSON.stringify(body.permissions), user.id);\n } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe, 201);\n });\n\n api.patch('/users/:id', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n validate(body, [\n { field: 'email', type: 'email' },\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'role', type: 'string', pattern: /^(owner|admin|member|viewer)$/ },\n ]);\n\n const user = await db.updateUser(c.req.param('id'), body);\n\n // Update client_org_id if provided\n if ('clientOrgId' in body) {\n const orgVal = body.clientOrgId || null;\n try {\n await (db as any).pool.query('UPDATE users SET client_org_id = $1 WHERE id = $2', [orgVal, user.id]);\n } catch {\n try { const edb = (db as any).db; if (edb?.prepare) edb.prepare('UPDATE users SET client_org_id = ? WHERE id = ?').run(orgVal, user.id); } catch { /* ignore */ }\n }\n }\n\n const { passwordHash, ...safe } = user;\n return c.json(safe);\n });\n\n // ─── Reset Password (admin/owner can reset any user's password) ──\n\n api.post('/users/:id/reset-password', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const newPassword = body.password;\n\n if (!newPassword || typeof newPassword !== 'string' || newPassword.length < 8) {\n return c.json({ error: 'Password must be at least 8 characters' }, 400);\n }\n\n const { default: bcrypt } = await import('bcryptjs');\n const passwordHash = await bcrypt.hash(newPassword, 12);\n\n // Use raw SQL via the pool — updateUser doesn't handle password_hash\n await (db as any).pool.query(\n 'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',\n [passwordHash, c.req.param('id')]\n );\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.password_reset',\n resource: `user:${c.req.param('id')}`,\n details: { targetEmail: existing.email, resetBy: 'admin' },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'Password reset successfully' });\n });\n\n // ─── Deactivate / Reactivate User ──────────────────\n\n api.post('/users/:id/deactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot deactivate your own account' }, 400);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User deactivated' });\n });\n\n api.post('/users/:id/reactivate', requireRole('admin'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n try {\n await (db as any).pool.query('UPDATE users SET is_active = TRUE, updated_at = NOW() WHERE id = $1', [c.req.param('id')]);\n } catch {\n const edb = (db as any).db;\n if (edb?.prepare) edb.prepare('UPDATE users SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(c.req.param('id'));\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.reactivated',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, message: 'User reactivated' });\n });\n\n // ─── Delete User (owner only, requires confirmation token) ──\n\n api.delete('/users/:id', requireRole('owner'), async (c) => {\n const existing = await db.getUser(c.req.param('id'));\n if (!existing) return c.json({ error: 'User not found' }, 404);\n\n const requesterId = c.get('userId');\n if (requesterId === c.req.param('id')) return c.json({ error: 'Cannot delete your own account' }, 400);\n\n // Require confirmation token from frontend (5-step modal flow)\n const body = await c.req.json().catch(() => ({}));\n if (body.confirmationToken !== 'DELETE_USER_' + existing.email) {\n return c.json({ error: 'Invalid confirmation. Delete requires 5-step confirmation from the dashboard.' }, 400);\n }\n\n await db.deleteUser(c.req.param('id'));\n\n await db.logEvent({\n actor: c.get('userId') || 'system', actorType: 'user', action: 'user.deleted',\n resource: `user:${c.req.param('id')}`, details: { targetEmail: existing.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true });\n });\n\n // ─── Page Registry (for permission UI) ──────────────\n\n api.get('/page-registry', requireRole('admin'), async (c) => {\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n return c.json(PAGE_REGISTRY);\n });\n\n // ─── User Permissions ──────────────────────────────\n\n api.get('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n return c.json({ userId: c.req.param('id'), permissions: user.permissions ?? '*' });\n });\n\n api.put('/users/:id/permissions', requireRole('admin'), async (c) => {\n const user = await db.getUser(c.req.param('id'));\n if (!user) return c.json({ error: 'User not found' }, 404);\n\n const body = await c.req.json();\n const permissions = body.permissions;\n\n // Validate: must be '*' or an object of pageId → true | string[]\n if (permissions !== '*') {\n if (typeof permissions !== 'object' || permissions === null || Array.isArray(permissions)) {\n return c.json({ error: 'permissions must be \"*\" or an object mapping pageId to true or string[]' }, 400);\n }\n const { PAGE_REGISTRY } = await import('./page-registry.js');\n for (const [pageId, grant] of Object.entries(permissions)) {\n if (pageId === '_allowedAgents') {\n // Validate: must be '*' or string[]\n if (grant !== '*' && !Array.isArray(grant)) {\n return c.json({ error: '_allowedAgents must be \"*\" or string[]' }, 400);\n }\n continue;\n }\n if (!(pageId in PAGE_REGISTRY)) {\n return c.json({ error: `Unknown page: ${pageId}` }, 400);\n }\n if (grant !== true && !Array.isArray(grant)) {\n return c.json({ error: `Permission for \"${pageId}\" must be true or string[]` }, 400);\n }\n }\n }\n\n const serialized = JSON.stringify(permissions);\n try {\n await (db as any).pool.query(\n 'UPDATE users SET permissions = $1, updated_at = NOW() WHERE id = $2',\n [serialized, c.req.param('id')]\n );\n } catch {\n // SQLite/other fallback\n const edb = (db as any).db || (db as any).pool;\n if (edb?.prepare) {\n edb.prepare('UPDATE users SET permissions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(serialized, c.req.param('id'));\n }\n }\n\n await db.logEvent({\n actor: c.get('userId') || 'system',\n actorType: 'user',\n action: 'user.permissions_updated',\n resource: `user:${c.req.param('id')}`,\n details: { permissions, targetEmail: user.email },\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim(),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, permissions });\n });\n\n // ─── Current User Permissions (for frontend filtering) ──\n\n api.get('/me/permissions', async (c) => {\n const userId = c.get('userId' as any);\n const userRole = c.get('userRole' as any);\n if (!userId) return c.json({ error: 'Not authenticated' }, 401);\n\n const user = await db.getUser(userId);\n const clientOrgId = user?.clientOrgId || c.get('clientOrgId' as any) || null;\n\n // Owner and admin always get full access\n if (userRole === 'owner' || userRole === 'admin') {\n return c.json({ permissions: '*', role: userRole, clientOrgId });\n }\n\n // Client org users get restricted page access by default\n if (clientOrgId) {\n const userPerms = user?.permissions;\n // Default client org pages — hide internal-only pages\n const clientPages: Record = {\n dashboard: true, agents: true, roles: true, skills: true,\n 'community-skills': true, 'skill-connections': true, 'database-access': true,\n knowledge: true, 'knowledge-contributions': true, 'memory-transfer': true,\n approvals: true, 'org-chart': true, 'task-pipeline': true, workforce: true,\n messages: true, guardrails: true, journal: true, activity: true,\n dlp: true, compliance: true, vault: true, audit: true, settings: true,\n };\n\n // Check org-level allowed_pages — merge additional pages granted by parent org\n try {\n const engineDb = db.getEngineDB();\n let orgRow: any;\n if ((db as any)._query) {\n const { rows } = await (db as any)._query(`SELECT allowed_pages FROM client_organizations WHERE id = $1`, [clientOrgId]);\n orgRow = rows?.[0];\n } else if (engineDb) {\n orgRow = await engineDb.get(`SELECT allowed_pages FROM client_organizations WHERE id = ?`, [clientOrgId]);\n }\n if (orgRow?.allowed_pages) {\n const extraPages = typeof orgRow.allowed_pages === 'string' ? JSON.parse(orgRow.allowed_pages) : orgRow.allowed_pages;\n if (Array.isArray(extraPages)) {\n extraPages.forEach((p: string) => { clientPages[p] = true; });\n }\n }\n } catch {}\n\n if (userPerms && userPerms !== '*') {\n return c.json({ permissions: userPerms, role: userRole, clientOrgId });\n }\n return c.json({ permissions: clientPages, role: userRole, clientOrgId });\n }\n\n return c.json({ permissions: user?.permissions ?? '*', role: userRole, clientOrgId });\n });\n\n // ─── Platform Capabilities ──────────────────────────\n\n api.get('/platform-capabilities', requireRole('admin'), async (c) => {\n const os = await import('node:os');\n const settings = await db.getSettings();\n return c.json({ capabilities: settings?.platformCapabilities || {}, serverOS: os.platform() });\n });\n\n api.put('/platform-capabilities', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n const userId = c.get('userId') || 'system';\n const capabilities = {\n localSystemAccess: !!body.localSystemAccess,\n telegram: !!body.telegram,\n whatsapp: !!body.whatsapp,\n enabledAt: new Date().toISOString(),\n enabledBy: userId,\n };\n\n await updateSettingsAndEmit({ platformCapabilities: capabilities } as any);\n\n // Also emit per-capability events for services that listen specifically\n for (const [cap, enabled] of Object.entries(body)) {\n if (cap === 'enabledAt' || cap === 'enabledBy') continue;\n configBus.emitCapability(cap, !!enabled);\n }\n\n await db.logEvent({\n actor: userId,\n actorType: 'user',\n action: 'platform.capabilities_updated',\n resource: 'company_settings',\n details: capabilities,\n ip: c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip'),\n orgId: c.get('userOrgId' as any) || undefined,\n }).catch(() => {});\n\n return c.json({ ok: true, capabilities });\n });\n\n // ─── WhatsApp QR Code ────────────────────────────────\n\n api.get('/whatsapp/qr/:agentId', requireRole('admin'), async (c) => {\n try {\n var { getWhatsAppQR, isWhatsAppConnected } = await import('../agent-tools/tools/messaging/whatsapp.js');\n var agentId = c.req.param('agentId');\n if (isWhatsAppConnected(agentId)) {\n return c.json({ status: 'connected' });\n }\n var qr = getWhatsAppQR(agentId);\n if (qr) {\n return c.json({ status: 'awaiting_scan', qr });\n }\n return c.json({ status: 'not_initialized', message: 'Agent has not started WhatsApp connection yet.' });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Audit Log ──────────────────────────────────────\n\n api.get('/audit', requireRole('admin'), async (c) => {\n const filters = {\n actor: c.req.query('actor') || undefined,\n action: c.req.query('action') || undefined,\n resource: c.req.query('resource') || undefined,\n orgId: c.req.query('orgId') || undefined,\n from: c.req.query('from') ? new Date(c.req.query('from')!) : undefined,\n to: c.req.query('to') ? new Date(c.req.query('to')!) : undefined,\n limit: Math.min(parseInt(c.req.query('limit') || '50'), 500),\n offset: Math.max(parseInt(c.req.query('offset') || '0'), 0),\n };\n\n // Validate date params\n if (filters.from && isNaN(filters.from.getTime())) {\n return c.json({ error: 'Invalid \"from\" date' }, 400);\n }\n if (filters.to && isNaN(filters.to.getTime())) {\n return c.json({ error: 'Invalid \"to\" date' }, 400);\n }\n\n const result = await db.queryAudit(filters);\n return c.json(result);\n });\n\n // ─── API Keys ───────────────────────────────────────\n\n api.get('/api-keys', requireRole('admin'), async (c) => {\n const keys = await db.listApiKeys();\n // Never expose key hashes\n const safe = keys.map(({ keyHash, ...k }) => k);\n return c.json({ keys: safe });\n });\n\n api.post('/api-keys', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 64 },\n ]);\n\n const userId = c.get('userId') || 'system';\n const scopes = Array.isArray(body.scopes) ? body.scopes : ['*'];\n const expiresAt = body.expiresAt ? new Date(body.expiresAt) : undefined;\n\n const { key, plaintext } = await db.createApiKey({\n name: body.name,\n scopes,\n createdBy: userId,\n expiresAt,\n });\n\n // Only time the plaintext key is returned — emphasize this\n const { keyHash, ...safeKey } = key;\n return c.json({\n key: safeKey,\n plaintext,\n warning: 'Store this key securely. It will not be shown again.',\n }, 201);\n });\n\n api.delete('/api-keys/:id', requireRole('admin'), async (c) => {\n const existing = await db.getApiKey(c.req.param('id'));\n if (!existing) return c.json({ error: 'API key not found' }, 404);\n\n await db.revokeApiKey(c.req.param('id'));\n return c.json({ ok: true, revoked: true });\n });\n\n // ─── Email Rules ────────────────────────────────────\n\n api.get('/rules', async (c) => {\n const agentId = c.req.query('agentId') || undefined;\n const rules = await db.getRules(agentId);\n return c.json({ rules });\n });\n\n api.post('/rules', async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n ]);\n\n // Validate conditions/actions are objects\n if (body.conditions && typeof body.conditions !== 'object') {\n return c.json({ error: 'conditions must be an object' }, 400);\n }\n if (body.actions && typeof body.actions !== 'object') {\n return c.json({ error: 'actions must be an object' }, 400);\n }\n\n const rule = await db.createRule({\n name: body.name,\n agentId: body.agentId,\n conditions: body.conditions || {},\n actions: body.actions || {},\n priority: body.priority ?? 0,\n enabled: body.enabled ?? true,\n });\n return c.json(rule, 201);\n });\n\n api.patch('/rules/:id', async (c) => {\n const body = await c.req.json();\n const rule = await db.updateRule(c.req.param('id'), body);\n return c.json(rule);\n });\n\n api.delete('/rules/:id', async (c) => {\n await db.deleteRule(c.req.param('id'));\n return c.json({ ok: true });\n });\n\n // ─── Settings ───────────────────────────────────────\n\n api.get('/settings', async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ error: 'Not configured' }, 404);\n\n // Redact sensitive fields\n const safe = { ...settings } as any;\n if (safe.smtpPass) safe.smtpPass = '***';\n if (safe.dkimPrivateKey) safe.dkimPrivateKey = '***';\n // Redact SSO secrets\n if (safe.ssoConfig?.oidc?.clientSecret) {\n safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: '***' } };\n }\n return c.json(safe);\n });\n\n api.patch('/settings', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'domain', type: 'string', maxLength: 253 },\n { field: 'subdomain', type: 'string', maxLength: 64 },\n { field: 'primaryColor', type: 'string', pattern: /^#[0-9a-fA-F]{6}$/ },\n { field: 'logoUrl', type: 'url' },\n { field: 'smtpHost', type: 'string', maxLength: 253 },\n { field: 'smtpPort', type: 'number' },\n { field: 'smtpUser', type: 'string', maxLength: 253 },\n { field: 'smtpPass', type: 'string', maxLength: 253 },\n { field: 'dkimPrivateKey', type: 'string' },\n { field: 'cfApiToken', type: 'string', maxLength: 500 },\n { field: 'cfAccountId', type: 'string', maxLength: 100 },\n { field: 'plan', type: 'string', maxLength: 32 },\n { field: 'signatureTemplate', type: 'string', maxLength: 10000 },\n { field: 'branding', type: 'object' },\n ]);\n\n const settings = await updateSettingsAndEmit(body);\n return c.json(settings);\n });\n\n // ─── Branding Asset Upload ──────────────────────────\n\n api.post('/settings/branding', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string\n if (!type || !data) return c.json({ error: 'type and data are required' }, 400);\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const os = await import('node:os');\n const fs = await import('node:fs');\n const path = await import('node:path');\n\n const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');\n if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });\n\n // Decode base64 (strip data URL prefix if present)\n const base64 = data.replace(/^data:[^;]+;base64,/, '');\n const buffer = Buffer.from(base64, 'base64');\n\n // Determine extension from filename or data URL\n const ext = filename ? path.extname(filename).toLowerCase() : '.png';\n const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];\n if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);\n\n // Save original\n const savedName = type + ext;\n fs.writeFileSync(path.join(brandDir, savedName), buffer);\n\n // Auto-generate favicon from logo upload\n if (type === 'logo' || type === 'favicon') {\n try {\n // Generate multiple sizes for favicon\n const sharp = (await import('sharp')).default;\n const sizes = [16, 32, 48, 64, 180, 192, 512];\n for (const size of sizes) {\n await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));\n }\n // Generate ICO (just use 32px PNG as simple favicon)\n await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));\n // Apple touch icon\n await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));\n } catch (e: any) {\n console.warn('[branding] Sharp not available, skipping icon generation:', e.message);\n // Still save the original — just won't have auto-generated sizes\n }\n }\n\n // Save branding config to settings (with cache-busting timestamp)\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n const v = Date.now();\n (branding as any)[type] = `/branding/${savedName}?v=${v}`;\n if (type === 'logo' || type === 'favicon') {\n (branding as any).favicon = `/branding/favicon.png?v=${v}`;\n (branding as any).appleTouchIcon = `/branding/apple-touch-icon.png?v=${v}`;\n (branding as any).icon192 = `/branding/icon-192.png?v=${v}`;\n (branding as any).icon512 = `/branding/icon-512.png?v=${v}`;\n }\n await updateSettingsAndEmit({ branding });\n\n return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });\n });\n\n api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {\n const type = c.req.param('type');\n if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);\n\n const settings = await db.getSettings();\n const branding = settings?.branding || {};\n delete (branding as any)[type];\n // If removing logo, also remove auto-generated icons\n if (type === 'logo') {\n delete (branding as any).favicon;\n delete (branding as any).appleTouchIcon;\n delete (branding as any).icon192;\n delete (branding as any).icon512;\n }\n await updateSettingsAndEmit({ branding });\n return c.json({ success: true, branding });\n });\n\n // ─── SSO Configuration ────────────────────────────\n\n api.get('/settings/sso', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n if (!settings) return c.json({ ssoConfig: null });\n\n const sso = settings.ssoConfig || {};\n // Redact secrets for display\n const safe = { ...sso } as any;\n if (safe.oidc?.clientSecret) {\n safe.oidc = { ...safe.oidc, clientSecret: '***' };\n }\n if (safe.saml?.certificate) {\n // Show first/last 20 chars of cert\n const cert = safe.saml.certificate;\n safe.saml = {\n ...safe.saml,\n certificate: cert.length > 50\n ? cert.substring(0, 20) + '...' + cert.substring(cert.length - 20)\n : cert,\n certificateConfigured: true,\n };\n }\n return c.json({ ssoConfig: safe });\n });\n\n api.put('/settings/sso/saml', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'entityId', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'ssoUrl', type: 'url', required: true },\n { field: 'certificate', type: 'string', required: true, minLength: 10 },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = {\n ...current,\n saml: {\n entityId: body.entityId,\n ssoUrl: body.ssoUrl,\n certificate: body.certificate,\n signatureAlgorithm: body.signatureAlgorithm || 'RSA-SHA256',\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'saml', configured: true });\n });\n\n api.put('/settings/sso/oidc', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'clientId', type: 'string', required: true, minLength: 1, maxLength: 256 },\n { field: 'clientSecret', type: 'string', required: true, minLength: 1, maxLength: 512 },\n { field: 'discoveryUrl', type: 'url', required: true },\n ]);\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n\n // If clientSecret is '***', keep the existing one\n let clientSecret = body.clientSecret;\n if (clientSecret === '***' && current.oidc?.clientSecret) {\n clientSecret = current.oidc.clientSecret;\n }\n\n const ssoConfig = {\n ...current,\n oidc: {\n clientId: body.clientId,\n clientSecret,\n discoveryUrl: body.discoveryUrl,\n scopes: body.scopes || ['openid', 'email', 'profile'],\n autoProvision: body.autoProvision ?? true,\n defaultRole: body.defaultRole || 'member',\n allowedDomains: body.allowedDomains || [],\n },\n };\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider: 'oidc', configured: true });\n });\n\n api.delete('/settings/sso/:provider', requireRole('admin'), async (c) => {\n const provider = c.req.param('provider');\n if (provider !== 'saml' && provider !== 'oidc') {\n return c.json({ error: 'Invalid provider. Use \"saml\" or \"oidc\".' }, 400);\n }\n\n const settings = await db.getSettings();\n const current = settings?.ssoConfig || {};\n const ssoConfig = { ...current };\n delete (ssoConfig as any)[provider];\n\n await updateSettingsAndEmit({ ssoConfig } as any);\n return c.json({ ok: true, provider, removed: true });\n });\n\n // Test OIDC discovery URL\n api.post('/settings/sso/oidc/test', requireRole('admin'), async (c) => {\n const { discoveryUrl } = await c.req.json();\n if (!discoveryUrl) return c.json({ error: 'discoveryUrl required' }, 400);\n\n try {\n const res = await fetch(discoveryUrl);\n if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });\n const doc = await res.json();\n\n return c.json({\n ok: true,\n issuer: doc.issuer,\n hasAuthorizationEndpoint: !!doc.authorization_endpoint,\n hasTokenEndpoint: !!doc.token_endpoint,\n hasUserinfoEndpoint: !!doc.userinfo_endpoint,\n hasJwksUri: !!doc.jwks_uri,\n supportedScopes: doc.scopes_supported,\n });\n } catch (e: any) {\n return c.json({ ok: false, error: e.message });\n }\n });\n\n // ─── Organization Email Config ─────────────────────\n\n api.get('/settings/org-email', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n const cfg = settings?.orgEmailConfig;\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: cfg.configured || false,\n provider: cfg.provider,\n label: cfg.label,\n oauthClientId: cfg.oauthClientId,\n oauthTenantId: cfg.oauthTenantId,\n });\n });\n\n api.put('/settings/org-email', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;\n if (!provider || !['google', 'microsoft'].includes(provider)) {\n return c.json({ error: 'provider must be \"google\" or \"microsoft\"' }, 400);\n }\n if (!oauthClientId || !oauthClientSecret) {\n return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);\n }\n const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';\n const orgEmailConfig = {\n provider,\n oauthClientId,\n oauthClientSecret,\n oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,\n oauthRedirectUri: '', // Will be set per-agent at OAuth time\n configured: true,\n label,\n };\n await updateSettingsAndEmit({ orgEmailConfig } as any);\n return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });\n });\n\n api.delete('/settings/org-email', requireRole('admin'), async (c) => {\n await updateSettingsAndEmit({ orgEmailConfig: null } as any);\n return c.json({ success: true });\n });\n\n // ─── Tool Security Config ─────────────────────────\n\n api.get('/settings/tool-security', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n api.put('/settings/tool-security', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n // Validate top-level shape\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n await updateSettingsAndEmit({ toolSecurityConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });\n });\n\n // ─── Firewall Config ──────────────────────────────────\n\n api.get('/settings/firewall', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.put('/settings/firewall', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (body && typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate mode fields\n if (body.ipAccess?.mode && !['allowlist', 'blocklist'].includes(body.ipAccess.mode)) {\n return c.json({ error: 'ipAccess.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n if (body.egress?.mode && !['allowlist', 'blocklist'].includes(body.egress.mode)) {\n return c.json({ error: 'egress.mode must be \"allowlist\" or \"blocklist\"' }, 400);\n }\n // Validate CIDR entries\n const { isValidIpOrCidr } = await import('../lib/cidr.js');\n for (const entry of (body.ipAccess?.allowlist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in allowlist: ' + entry }, 400);\n }\n for (const entry of (body.ipAccess?.blocklist || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in blocklist: ' + entry }, 400);\n }\n for (const entry of (body.trustedProxies?.ips || [])) {\n if (!isValidIpOrCidr(entry)) return c.json({ error: 'Invalid IP/CIDR in trusted proxies: ' + entry }, 400);\n }\n // Self-lockout protection for allowlist mode\n if (body.ipAccess?.enabled && body.ipAccess?.mode === 'allowlist' && body.ipAccess?.allowlist?.length > 0) {\n const clientIp = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() || c.req.header('x-real-ip') || '';\n if (clientIp && clientIp !== 'unknown') {\n const { compileIpMatcher } = await import('../lib/cidr.js');\n const matcher = compileIpMatcher(body.ipAccess.allowlist);\n if (!matcher(clientIp)) {\n return c.json({ error: 'Your current IP (' + clientIp + ') is not in the allowlist. Add it first to avoid lockout.' }, 400);\n }\n }\n }\n await updateSettingsAndEmit({ firewallConfig: body } as any);\n // Hot-reload ALL network middleware (firewall, security headers, rate limiting, HTTPS, egress, proxy)\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n const settings = await db.getSettings();\n return c.json({ firewallConfig: settings?.firewallConfig || {} });\n });\n\n api.post('/settings/firewall/test-ip', requireRole('admin'), async (c) => {\n const { ip } = await c.req.json();\n if (!ip) return c.json({ error: 'ip is required' }, 400);\n const { isValidIpOrCidr, compileIpMatcher } = await import('../lib/cidr.js');\n if (!isValidIpOrCidr(ip)) return c.json({ error: 'Invalid IP address' }, 400);\n const settings = await db.getSettings();\n const ipAccess = settings?.firewallConfig?.ipAccess;\n if (!ipAccess?.enabled) {\n return c.json({ ip, allowed: true, reason: 'IP access control is disabled' });\n }\n if (ipAccess.mode === 'allowlist') {\n const matcher = compileIpMatcher(ipAccess.allowlist || []);\n const allowed = matcher(ip);\n return c.json({ ip, allowed, reason: allowed ? 'IP matches allowlist' : 'IP not in allowlist' });\n } else {\n const matcher = compileIpMatcher(ipAccess.blocklist || []);\n const blocked = matcher(ip);\n return c.json({ ip, allowed: !blocked, reason: blocked ? 'IP matches blocklist' : 'IP not in blocklist' });\n }\n });\n\n // ─── Model Pricing Config ──────────────────────────────\n\n api.get('/settings/model-pricing', requireRole('admin'), async (c) => {\n const settings = await db.getSettings();\n var config = settings?.modelPricingConfig || { models: [], currency: 'USD' };\n // Pre-seed with defaults if empty\n if (!config.models || config.models.length === 0) {\n config.models = getDefaultModelPricing();\n }\n return c.json({ modelPricingConfig: config });\n });\n\n api.put('/settings/model-pricing', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n if (!body || typeof body !== 'object') {\n return c.json({ error: 'Body must be a JSON object' }, 400);\n }\n // Validate models array\n if (body.models && Array.isArray(body.models)) {\n for (const m of body.models) {\n if (!m.provider || !m.modelId) {\n return c.json({ error: 'Each model must have provider and modelId' }, 400);\n }\n if (typeof m.inputCostPerMillion !== 'number' || m.inputCostPerMillion < 0) {\n return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);\n }\n if (typeof m.outputCostPerMillion !== 'number' || m.outputCostPerMillion < 0) {\n return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);\n }\n }\n }\n body.updatedAt = new Date().toISOString();\n await updateSettingsAndEmit({ modelPricingConfig: body } as any);\n const settings = await db.getSettings();\n return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });\n });\n\n // ─── Provider Management ─────────────────────────────\n\n api.get('/providers', requireRole('admin'), async (c) => {\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var savedApiKeys = pricingConfig?.providerApiKeys || {};\n var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {\n var configured = !p.requiresApiKey || !!savedApiKeys[p.id];\n return {\n id: p.id,\n name: p.name,\n baseUrl: p.baseUrl,\n apiType: p.apiType,\n isLocal: p.isLocal,\n requiresApiKey: p.requiresApiKey,\n configured: configured,\n source: 'built-in' as const,\n defaultModels: p.defaultModels || [],\n };\n });\n\n var customProviders = pricingConfig?.customProviders || [];\n var custom = customProviders.map(function(p: any) {\n return { ...p, configured: true, source: 'custom' as const };\n });\n\n return c.json({ providers: [...builtIn, ...custom] });\n });\n\n api.post('/providers', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.id || !body.name || !body.baseUrl || !body.apiType) {\n return c.json({ error: 'id, name, baseUrl, and apiType are required' }, 400);\n }\n if (PROVIDER_REGISTRY[body.id]) {\n return c.json({ error: 'Cannot override built-in provider' }, 409);\n }\n var validTypes = ['anthropic', 'openai-compatible', 'google', 'ollama'];\n if (!validTypes.includes(body.apiType)) {\n return c.json({ error: 'apiType must be one of: ' + validTypes.join(', ') }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n if (config.customProviders.find(function(p: any) { return p.id === body.id; })) {\n return c.json({ error: 'Custom provider with this ID already exists' }, 409);\n }\n\n config.customProviders.push({\n id: body.id,\n name: body.name,\n baseUrl: body.baseUrl,\n apiType: body.apiType,\n apiKeyEnvVar: body.apiKeyEnvVar || '',\n headers: body.headers || {},\n models: body.models || [],\n });\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: body });\n });\n\n // ─── Provider API Key Management ────────────────────────\n api.post('/providers/:id/api-key', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n if (!provider) {\n return c.json({ error: 'Unknown provider' }, 404);\n }\n var body = await c.req.json();\n var apiKey = body.apiKey?.trim();\n if (!apiKey || typeof apiKey !== 'string' || apiKey.length < 5) {\n return c.json({ error: 'Valid API key required' }, 400);\n }\n var skipValidation = body.skipValidation === true;\n\n // Validate the API key against the provider before saving\n if (!skipValidation) {\n try {\n var valid = await validateProviderApiKey(id, apiKey, provider);\n if (!valid.ok) {\n return c.json({ error: 'API key validation failed: ' + valid.error, validationFailed: true }, 400);\n }\n } catch (e: any) {\n return c.json({ error: 'API key validation failed: ' + (e.message || 'Unknown error'), validationFailed: true }, 400);\n }\n }\n\n // Store API key encrypted via vault\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.providerApiKeys = config.providerApiKeys || {};\n config.providerApiKeys[id] = vault.encrypt(apiKey);\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n\n return c.json({ ok: true, message: 'API key saved for ' + provider.name, validated: !skipValidation });\n });\n\n api.put('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot modify built-in provider' }, 400);\n }\n\n var body = await c.req.json();\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var idx = config.customProviders.findIndex(function(p: any) { return p.id === id; });\n if (idx === -1) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id: id });\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true, provider: config.customProviders[idx] });\n });\n\n api.delete('/providers/:id', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n if (PROVIDER_REGISTRY[id]) {\n return c.json({ error: 'Cannot delete built-in provider' }, 400);\n }\n\n var settings = await db.getSettings();\n var config = (settings as any)?.modelPricingConfig || { models: [], currency: 'USD' };\n config.customProviders = config.customProviders || [];\n\n var before = config.customProviders.length;\n config.customProviders = config.customProviders.filter(function(p: any) { return p.id !== id; });\n\n if (config.customProviders.length === before) {\n return c.json({ error: 'Custom provider not found' }, 404);\n }\n\n await updateSettingsAndEmit({ modelPricingConfig: config } as any);\n return c.json({ ok: true });\n });\n\n api.get('/providers/:id/models', requireRole('admin'), async (c) => {\n var id = c.req.param('id');\n var provider = PROVIDER_REGISTRY[id];\n\n // Ollama auto-discovery\n if (id === 'ollama' || (provider && provider.apiType === 'ollama')) {\n var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : 'http://localhost:11434');\n try {\n var resp = await fetch(ollamaHost + '/api/tags', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.models || []).map(function(m: any) { return { id: m.name, name: m.name, size: m.size }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to Ollama: ' + err.message });\n }\n }\n\n // OpenAI-compatible local auto-discovery (vLLM, LM Studio, LiteLLM)\n if (provider && provider.isLocal && provider.apiType === 'openai-compatible') {\n try {\n var resp = await fetch(provider.baseUrl + '/models', { signal: AbortSignal.timeout(3000) });\n var data = await resp.json() as any;\n return c.json({ models: (data.data || []).map(function(m: any) { return { id: m.id, name: m.id }; }) });\n } catch (err: any) {\n return c.json({ models: [], error: 'Cannot connect to ' + provider.name + ': ' + err.message });\n }\n }\n\n // Cloud providers — return default models from registry\n if (provider && provider.defaultModels) {\n return c.json({ models: provider.defaultModels.map(function(mid: string) { return { id: mid, name: mid }; }) });\n }\n\n // Custom providers — check DB\n var settings = await db.getSettings();\n var pricingConfig = (settings as any)?.modelPricingConfig;\n var customProviders = pricingConfig?.customProviders || [];\n var customProvider = customProviders.find(function(p: any) { return p.id === id; });\n if (customProvider && customProvider.models) {\n return c.json({ models: customProvider.models });\n }\n\n return c.json({ models: [] });\n });\n\n // ─── Retention ──────────────────────────────────────\n\n api.get('/retention', requireRole('admin'), async (c) => {\n const policy = await db.getRetentionPolicy();\n return c.json(policy);\n });\n\n api.put('/retention', requireRole('owner'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'enabled', type: 'boolean', required: true },\n { field: 'retainDays', type: 'number', required: true, min: 1, max: 3650 },\n { field: 'archiveFirst', type: 'boolean' },\n ]);\n\n await db.setRetentionPolicy({\n enabled: body.enabled,\n retainDays: body.retainDays,\n excludeTags: body.excludeTags || [],\n archiveFirst: body.archiveFirst ?? true,\n });\n return c.json({ ok: true });\n });\n\n // ─── Security ────────────────────────────────────────\n\n api.get('/settings/security', requireRole('admin'), async (c) => {\n try {\n const settings = await db.getSettings();\n const securityConfig = (settings as any)?.securityConfig || {};\n return c.json({ securityConfig });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/settings/security', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const { securityConfig } = body;\n\n if (!securityConfig || typeof securityConfig !== 'object') {\n return c.json({ error: 'securityConfig is required and must be an object' }, 400);\n }\n\n await updateSettingsAndEmit({ securityConfig } as any);\n\n // Sync transport encryption config to middleware\n if (securityConfig.transportEncryption) {\n try {\n const { setTransportEncryptionConfig } = await import('../middleware/transport-encryption.js');\n setTransportEncryptionConfig(securityConfig.transportEncryption);\n } catch {}\n }\n\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/events', requireRole('admin'), async (c) => {\n try {\n const query = c.req.query();\n const filter = {\n eventType: query.eventType ? query.eventType.split(',') : undefined,\n severity: query.severity ? query.severity.split(',') : undefined,\n agentId: query.agentId,\n sourceIp: query.sourceIp,\n fromDate: query.fromDate,\n toDate: query.toDate,\n limit: query.limit ? parseInt(query.limit) : 50,\n offset: query.offset ? parseInt(query.offset) : 0\n };\n\n const events = await (db as any).getSecurityEvents(filter);\n return c.json({ events });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/settings/security/port-scan', requireRole('admin'), async (c) => {\n try {\n const { scanPorts } = await import('../security/port-scanner.js');\n const result = await scanPorts();\n return c.json({ scanResult: result });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.get('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const agent = await db.getAgent(agentId);\n \n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n const securityOverrides = (agent as any)?.securityOverrides || {};\n return c.json({ securityOverrides });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n api.put('/agents/:id/security', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('id');\n const body = await c.req.json();\n const { securityOverrides } = body;\n\n const agent = await db.getAgent(agentId);\n if (!agent) {\n return c.json({ error: 'Agent not found' }, 404);\n }\n\n await db.updateAgent(agentId, { securityOverrides } as any);\n return c.json({ ok: true });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── CORS Helper ────────────────────────────────────\n /** Add an origin to CORS list, optionally removing an old one */\n async function updateCorsOrigin(newOrigin: string, oldOrigin?: string) {\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? [...net.corsOrigins] : [];\n // Remove old if present\n if (oldOrigin) origins = origins.filter((o: string) => o !== oldOrigin);\n // Add new if not already present\n if (!origins.includes(newOrigin)) origins.push(newOrigin);\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch { /* non-critical */ }\n }\n\n // ─── Get CORS Origins ─────────────────────────────────\n api.get('/domain/cors', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var origins = settings?.firewallConfig?.network?.corsOrigins || [];\n return c.json({ origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Update CORS Origins ──────────────────────────────\n api.post('/domain/cors', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!Array.isArray(body.origins)) {\n return c.json({ error: 'origins must be an array of URLs' }, 400);\n }\n // Validate each origin\n for (var o of body.origins) {\n if (typeof o !== 'string') return c.json({ error: 'Each origin must be a string' }, 400);\n if (o !== '*' && !o.startsWith('http://') && !o.startsWith('https://')) {\n return c.json({ error: 'Origin \"' + o + '\" must start with http:// or https://' }, 400);\n }\n }\n try {\n var settings = await db.getSettings();\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: body.origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n return c.json({ success: true, origins: body.origins });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Registration ────────────────────────────\n\n api.post('/domain/register', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n // Generate deployment key\n var keyPair = await lock.generateDeploymentKey();\n\n // Get company info for registration\n var settings = await db.getSettings();\n\n // Register with central registry\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n // Store in settings\n await updateSettingsAndEmit({\n domain: domain,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n } as any);\n\n return c.json({\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain registration failed' }, 500);\n }\n });\n\n api.post('/domain/verify', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n\n var result = await lock.checkVerification(domain);\n\n if (result.verified) {\n await updateSettingsAndEmit({\n domainStatus: 'verified',\n domainVerifiedAt: new Date().toISOString(),\n } as any);\n return c.json({ verified: true });\n }\n\n return c.json({ verified: false, error: result.error });\n } catch (err: any) {\n return c.json({ error: err.message || 'Verification check failed' }, 500);\n }\n });\n\n // ─── Domain Status (GET) ──────────────────────────────\n api.get('/domain/status', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n return c.json({\n domain: settings.domain || null,\n subdomain: settings.subdomain || null,\n status: settings.domainStatus || 'unregistered',\n registeredAt: settings.domainRegisteredAt || null,\n verifiedAt: settings.domainVerifiedAt || null,\n dnsChallenge: settings.domainDnsChallenge || null,\n useRootDomain: settings.useRootDomain || false,\n plan: settings.plan || 'self-hosted',\n });\n } catch (err: any) {\n return c.json({ error: err.message }, 500);\n }\n });\n\n // ─── Domain Change ────────────────────────────────────\n api.post('/domain/change', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.domain) {\n return c.json({ error: 'domain is required' }, 400);\n }\n\n var domain = String(body.domain).toLowerCase().trim();\n if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/.test(domain)) {\n return c.json({ error: 'Invalid domain format' }, 400);\n }\n\n try {\n var { DomainLock } = await import('../domain-lock/index.js');\n var lock = new DomainLock();\n var keyPair = await lock.generateDeploymentKey();\n var settings = await db.getSettings();\n\n var result = await lock.register(domain, keyPair.hash, {\n orgName: settings?.name,\n contactEmail: body.contactEmail,\n });\n\n if (!result.success) {\n return c.json({ error: result.error, statusCode: result.statusCode }, 400);\n }\n\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: domain,\n useRootDomain: body.useRootDomain || false,\n deploymentKeyHash: keyPair.hash,\n domainRegistrationId: result.registrationId,\n domainDnsChallenge: result.dnsChallenge,\n domainRegisteredAt: new Date().toISOString(),\n domainStatus: 'pending_dns',\n domainVerifiedAt: undefined,\n } as any);\n\n // Auto-update CORS\n await updateCorsOrigin('https://' + domain, oldDomain ? 'https://' + oldDomain : undefined);\n\n return c.json({\n success: true,\n deploymentKey: keyPair.plaintext,\n dnsChallenge: result.dnsChallenge,\n registrationId: result.registrationId,\n });\n } catch (err: any) {\n return c.json({ error: err.message || 'Domain change failed' }, 500);\n }\n });\n\n // ─── Subdomain Update ─────────────────────────────────\n api.post('/domain/subdomain', requireRole('admin'), async (c) => {\n var body = await c.req.json();\n if (!body.subdomain) {\n return c.json({ error: 'subdomain is required' }, 400);\n }\n var sub = String(body.subdomain).toLowerCase().trim().replace(/\\.agenticmail\\.io$/, '');\n if (sub.length < 2) {\n return c.json({ error: 'Subdomain must be at least 2 characters.' }, 400);\n }\n if (sub.length > 63) {\n return c.json({ error: 'Subdomain must be 63 characters or fewer.' }, 400);\n }\n if (/^-|-$/.test(sub)) {\n return c.json({ error: 'Subdomain cannot start or end with a hyphen.' }, 400);\n }\n if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(sub)) {\n return c.json({ error: 'Subdomain can only contain lowercase letters, numbers, and hyphens.' }, 400);\n }\n // Reserved subdomains\n var reserved = ['www', 'mail', 'api', 'app', 'admin', 'dashboard', 'help', 'support', 'docs', 'status', 'blog', 'cdn', 'static', 'assets', 'ns1', 'ns2'];\n if (reserved.includes(sub)) {\n return c.json({ error: '\"' + sub + '\" is a reserved subdomain. Please choose a different one.' }, 400);\n }\n try {\n var settings = await db.getSettings();\n var oldSub = settings.subdomain || null;\n await updateSettingsAndEmit({ subdomain: sub } as any);\n // Auto-update CORS\n await updateCorsOrigin(\n 'https://' + sub + '.agenticmail.io',\n oldSub ? 'https://' + oldSub + '.agenticmail.io' : undefined,\n );\n return c.json({ success: true, subdomain: sub, oldSubdomain: oldSub, plan: settings.plan || 'self-hosted' });\n } catch (err: any) {\n return c.json({ error: err.message || 'Subdomain update failed' }, 500);\n }\n });\n\n // ─── Remove Custom Domain ─────────────────────────────\n api.delete('/domain', requireRole('admin'), async (c) => {\n try {\n var settings = await db.getSettings();\n var oldDomain = settings.domain;\n await updateSettingsAndEmit({\n domain: undefined,\n domainStatus: undefined,\n domainDnsChallenge: undefined,\n domainRegisteredAt: undefined,\n domainVerifiedAt: undefined,\n domainRegistrationId: undefined,\n deploymentKeyHash: undefined,\n } as any);\n // Remove old domain from CORS\n if (oldDomain) {\n try {\n var fw = settings.firewallConfig || {};\n var net = fw.network || {};\n var origins: string[] = Array.isArray(net.corsOrigins) ? net.corsOrigins.filter((o: string) => o !== 'https://' + oldDomain) : [];\n await updateSettingsAndEmit({ firewallConfig: { ...fw, network: { ...net, corsOrigins: origins } } } as any);\n try { const { invalidateNetworkConfig } = await import('../middleware/network-config.js'); await invalidateNetworkConfig(); } catch {}\n } catch {}\n }\n return c.json({ success: true });\n } catch (err: any) {\n return c.json({ error: err.message || 'Failed to remove domain' }, 500);\n }\n });\n\n function getDefaultModelPricing() {\n return [\n // Anthropic (Feb 2026 — 1M context window)\n { provider: 'anthropic', modelId: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-6', displayName: 'Claude Sonnet 4.6', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-sonnet-4-5-20250929', displayName: 'Claude Sonnet 4.5', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', displayName: 'Claude Haiku 4.5', inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 200000 },\n // OpenAI\n { provider: 'openai', modelId: 'gpt-4o', displayName: 'GPT-4o', inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128000 },\n { provider: 'openai', modelId: 'gpt-4.1', displayName: 'GPT-4.1', inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-mini', displayName: 'GPT-4.1 Mini', inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'gpt-4.1-nano', displayName: 'GPT-4.1 Nano', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'openai', modelId: 'o3', displayName: 'o3', inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 200000 },\n { provider: 'openai', modelId: 'o4-mini', displayName: 'o4-mini', inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 200000 },\n // Google Gemini (up to 2M context)\n { provider: 'google', modelId: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1000000 },\n { provider: 'google', modelId: 'gemini-3-pro', displayName: 'Gemini 3 Pro', inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1000000 },\n // DeepSeek (128K context)\n { provider: 'deepseek', modelId: 'deepseek-chat', displayName: 'DeepSeek Chat (V3)', inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128000 },\n { provider: 'deepseek', modelId: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner (R1)', inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128000 },\n // xAI Grok (2M context window)\n { provider: 'xai', modelId: 'grok-4', displayName: 'Grok 4', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-4-fast', displayName: 'Grok 4 Fast', inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2000000 },\n { provider: 'xai', modelId: 'grok-3', displayName: 'Grok 3', inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },\n { provider: 'xai', modelId: 'grok-3-mini', displayName: 'Grok 3 Mini', inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },\n // Mistral\n { provider: 'mistral', modelId: 'mistral-large-latest', displayName: 'Mistral Large', inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128000 },\n { provider: 'mistral', modelId: 'mistral-small-latest', displayName: 'Mistral Small', inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128000 },\n // Groq (inference provider)\n { provider: 'groq', modelId: 'llama-3.3-70b-versatile', displayName: 'Llama 3.3 70B (Groq)', inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128000 },\n // Together (inference provider)\n { provider: 'together', modelId: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B (Together)', inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128000 },\n ];\n }\n\n // ─── Cloudflare Tunnel Deployment ───────────────────\n\n /** Check if cloudflared is installed and tunnel status */\n api.get('/tunnel/status', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n let installed = false;\n let version = '';\n let running = false;\n let config: any = null;\n\n try {\n version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n installed = true;\n } catch { /* not installed */ }\n\n // Check if running via pm2\n try {\n const pm2List = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', timeout: 5000 });\n const procs = JSON.parse(pm2List);\n const cf = procs.find((p: any) => p.name === 'cloudflared');\n running = cf?.pm2_env?.status === 'online';\n } catch { /* pm2 not available */ }\n\n // Read config\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const cfgPath = path.join(cfDir, 'config.yml');\n if (fs.existsSync(cfgPath)) {\n const raw = fs.readFileSync(cfgPath, 'utf8');\n // Parse simple YAML\n const tunnelMatch = raw.match(/^tunnel:\\s*(.+)$/m);\n const hostnameMatch = raw.match(/hostname:\\s*(.+)$/m);\n const serviceMatch = raw.match(/service:\\s*(http.+)$/m);\n config = {\n tunnelId: tunnelMatch?.[1]?.trim(),\n hostname: hostnameMatch?.[1]?.trim(),\n service: serviceMatch?.[1]?.trim(),\n raw,\n };\n }\n\n return c.json({ installed, version, running, config });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Install cloudflared */\n api.post('/tunnel/install', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const platform = os.default.platform();\n\n if (platform === 'darwin') {\n // macOS — try brew first\n try {\n execSync(process.platform === 'win32' ? 'where brew' : 'which brew', { timeout: 3000 });\n execSync('brew install cloudflared 2>&1', { encoding: 'utf8', timeout: 120000 });\n } catch {\n // Direct download\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n }\n } else if (platform === 'linux') {\n const arch = os.default.arch() === 'arm64' ? 'arm64' : 'amd64';\n execSync(`curl -L -o /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch} && chmod +x /usr/local/bin/cloudflared`, { timeout: 60000 });\n } else {\n return c.json({ error: 'Unsupported platform: ' + platform }, 400);\n }\n\n const version = execSync('cloudflared --version 2>&1', { encoding: 'utf8', timeout: 5000 }).trim();\n return c.json({ success: true, version });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Authenticate with Cloudflare (opens browser for login) */\n api.post('/tunnel/login', requireRole('admin'), async (c) => {\n try {\n const { exec: execCb } = await import('child_process');\n const { promisify } = await import('util');\n const execP = promisify(execCb);\n // This opens the browser for CF login — cert.pem is saved to ~/.cloudflared/\n await execP('cloudflared tunnel login', { timeout: 120000 });\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: 'Login failed or timed out. Make sure to complete the browser authorization. ' + e.message }, 500);\n }\n });\n\n /** Create tunnel, configure DNS, and start */\n api.post('/tunnel/deploy', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n const { domain, tunnelName, port } = body;\n if (!domain) return c.json({ error: 'domain is required' }, 400);\n\n const localPort = port || 3200;\n const name = tunnelName || 'agenticmail-enterprise';\n\n try {\n const { execSync } = await import('child_process');\n const os = await import('os');\n const fs = await import('fs');\n const path = await import('path');\n const cfDir = path.join(os.default.homedir(), '.cloudflared');\n const steps: string[] = [];\n\n // Check cert exists (user must have logged in)\n if (!fs.existsSync(path.join(cfDir, 'cert.pem'))) {\n return c.json({ error: 'Not authenticated with Cloudflare. Click \"Login to Cloudflare\" first.' }, 400);\n }\n\n // 1. Create tunnel\n let tunnelId = '';\n try {\n const out = execSync(`cloudflared tunnel create ${name} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n const match = out.match(/Created tunnel .+ with id ([a-f0-9-]+)/);\n tunnelId = match?.[1] || '';\n steps.push('Created tunnel: ' + name + ' (' + tunnelId + ')');\n } catch (e: any) {\n // Tunnel might already exist\n if (e.message?.includes('already exists')) {\n const listOut = execSync('cloudflared tunnel list --output json 2>&1', { encoding: 'utf8', timeout: 15000 });\n const tunnels = JSON.parse(listOut);\n const existing = tunnels.find((t: any) => t.name === name);\n if (existing) {\n tunnelId = existing.id;\n steps.push('Using existing tunnel: ' + name + ' (' + tunnelId + ')');\n } else {\n throw e;\n }\n } else {\n throw e;\n }\n }\n\n if (!tunnelId) return c.json({ error: 'Failed to get tunnel ID' }, 500);\n\n // 2. Write config\n const config = [\n `tunnel: ${tunnelId}`,\n `credentials-file: ${path.join(cfDir, tunnelId + '.json')}`,\n '',\n 'ingress:',\n ` - hostname: ${domain}`,\n ` service: http://localhost:${localPort}`,\n ' - service: http_status:404',\n ].join('\\n');\n\n fs.writeFileSync(path.join(cfDir, 'config.yml'), config);\n steps.push('Wrote config: ' + domain + ' → localhost:' + localPort);\n\n // 3. Route DNS\n try {\n execSync(`cloudflared tunnel route dns ${tunnelId} ${domain} 2>&1`, { encoding: 'utf8', timeout: 30000 });\n steps.push('DNS CNAME created: ' + domain + ' → ' + tunnelId + '.cfargotunnel.com');\n } catch (e: any) {\n if (e.message?.includes('already exists')) {\n steps.push('DNS CNAME already exists for ' + domain);\n } else {\n steps.push('DNS routing failed (you may need to add CNAME manually): ' + e.message);\n }\n }\n\n // 4. Start with PM2\n try {\n execSync(process.platform === 'win32' ? 'where pm2' : 'which pm2', { timeout: 3000 });\n // Stop existing if any\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n execSync(`pm2 start cloudflared --name cloudflared -- tunnel run`, { encoding: 'utf8', timeout: 15000 });\n execSync('pm2 save 2>/dev/null', { timeout: 5000 });\n steps.push('Started cloudflared via PM2 (auto-restarts on crash)');\n } catch {\n // No PM2 — try running directly in background\n try {\n const { spawn } = await import('child_process');\n const child = spawn('cloudflared', ['tunnel', 'run'], { detached: true, stdio: 'ignore' });\n child.unref();\n steps.push('Started cloudflared in background (install PM2 for auto-restart)');\n } catch (e2: any) {\n steps.push('Could not start tunnel automatically: ' + e2.message);\n }\n }\n\n // 5. Update CORS to allow the new domain\n try {\n if (db) {\n const corsRows = await (db as any).query(`SELECT value FROM admin_settings WHERE key = 'cors_origins'`);\n let origins: string[] = [];\n if (corsRows?.[0]) {\n try { origins = JSON.parse((corsRows[0] as any).value); } catch { origins = []; }\n }\n const newOrigin = 'https://' + domain;\n if (!origins.includes(newOrigin)) {\n origins.push(newOrigin);\n await (db as any).execute(\n `INSERT INTO admin_settings (key, value) VALUES ('cors_origins', $1) ON CONFLICT (key) DO UPDATE SET value = $1`,\n [JSON.stringify(origins)]\n );\n steps.push('Added ' + newOrigin + ' to CORS allowed origins');\n }\n }\n } catch { /* non-critical */ }\n\n return c.json({ success: true, tunnelId, domain, steps });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Client Organizations ─────────────────────────────\n\n api.get('/organizations', async (c) => {\n try {\n // Org-bound users can only see their own organization\n const clientOrgId = c.get('clientOrgId' as any);\n if (clientOrgId) {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT o.*, COUNT(a.id) as agent_count FROM client_organizations o LEFT JOIN agents a ON a.client_org_id = o.id WHERE o.id = $1 GROUP BY o.id`, [clientOrgId]);\n return c.json({ organizations: rows });\n }\n return c.json({ organizations: [] });\n }\n\n // Full access for admins/owners\n const userRole = c.get('userRole' as any);\n if (userRole !== 'admin' && userRole !== 'owner') {\n return c.json({ error: 'Insufficient permissions' }, 403);\n }\n\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n } else {\n const engineDb = db.getEngineDB();\n const rows = await engineDb!.all(`\n SELECT o.*, COUNT(a.id) as agent_count\n FROM client_organizations o\n LEFT JOIN agents a ON a.client_org_id = o.id\n GROUP BY o.id\n ORDER BY o.created_at DESC\n `);\n return c.json({ organizations: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations', requireRole('admin'), async (c) => {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n const id = (await import('crypto')).randomUUID();\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0], 201);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(\n `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description, billing_rate_per_agent, currency) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null, body.billing_rate_per_agent || 0, body.currency || 'USD']\n );\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row, 201);\n }\n } catch (e: any) {\n if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.get('/organizations/:id', async (c) => {\n const id = c.req.param('id');\n const userRole = c.get('userRole' as any);\n const userClientOrgId = c.get('clientOrgId' as any);\n // Non-admins can only access their own client org\n if (userRole !== 'owner' && userRole !== 'admin') {\n if (!userClientOrgId || userClientOrgId !== id) return c.json({ error: 'Forbidden' }, 403);\n }\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);\n const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);\n return c.json({ ...orgs[0], agents });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);\n return c.json({ ...(org as any), agents });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.patch('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', minLength: 1, maxLength: 128 },\n { field: 'contact_name', type: 'string', maxLength: 128 },\n { field: 'contact_email', type: 'email' },\n { field: 'description', type: 'string', maxLength: 512 },\n ]);\n try {\n const fields: string[] = [];\n const values: any[] = [];\n const isPostgres = (db as any).pool;\n let idx = 1;\n for (const key of ['name', 'contact_name', 'contact_email', 'description', 'billing_rate_per_agent', 'currency']) {\n if (body[key] !== undefined) {\n fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);\n values.push(body[key]);\n }\n }\n // JSON fields\n if (body.allowed_roles !== undefined) {\n fields.push(isPostgres ? `allowed_roles = $${idx++}` : `allowed_roles = ?`);\n values.push(JSON.stringify(body.allowed_roles));\n }\n if (body.allowed_skills !== undefined) {\n fields.push(isPostgres ? `allowed_skills = $${idx++}` : `allowed_skills = ?`);\n values.push(JSON.stringify(body.allowed_skills));\n }\n if (body.allowed_pages !== undefined) {\n fields.push(isPostgres ? `allowed_pages = $${idx++}` : `allowed_pages = ?`);\n values.push(JSON.stringify(body.allowed_pages));\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = CURRENT_TIMESTAMP`);\n values.push(id);\n const where = isPostgres ? `$${idx}` : '?';\n const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;\n if (isPostgres) {\n await (db as any)._query(sql, values);\n const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);\n return c.json(rows[0]);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql, values);\n const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);\n return c.json(row);\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);\n if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !rows[0].is_active;\n await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);\n return c.json({ is_active: newActive });\n } else {\n const engineDb = db.getEngineDB();\n const org = await engineDb!.get(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);\n if (!org) return c.json({ error: 'Organization not found' }, 404);\n const newActive = !(org.is_active);\n await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [newActive ? 1 : 0, id]);\n const newStatus = newActive ? 'active' : 'suspended';\n await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);\n return c.json({ is_active: newActive });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.delete('/organizations/:id', requireRole('admin'), async (c) => {\n const id = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);\n } else {\n const engineDb = db.getEngineDB();\n const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);\n if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);\n await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent-Org Linking ──────────────────────────────────\n\n api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { orgId, clearCredentials } = await c.req.json();\n if (!orgId) return c.json({ error: 'orgId is required' }, 400);\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org to detect reassignment\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n const isReassignment = previousOrgId && previousOrgId !== orgId;\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = $1, updated_at = NOW() WHERE id = $2`, [orgId, agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [orgId, agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // ALWAYS clear agent credentials when assigning to an org\n // Agent should start fresh with the new org's inherited credentials\n let credentialsCleared = 0;\n if (clearCredentials !== false) {\n try {\n // Clear agent-level email config from DB\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n // SQLite: read-modify-write\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n // Clear per-agent vault secrets from previous org (if reassignment)\n if (previousOrgId && (globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* vault may not support this query */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear in-memory + push new org's credentials to the running agent\n let credentialsPushed = false;\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n // Force-clear ALL email config from running agent (in-memory)\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n // Update the agent's client_org_id in-memory\n if (agent) {\n agent.client_org_id = orgId;\n agent.clientOrgId = orgId;\n }\n // Push new org's credentials\n credentialsPushed = await oi.pushCredentialsToAgent(agentId, orgId);\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, reassigned: !!isReassignment, previousOrgId, credentialsCleared, credentialsPushed });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n\n // Get current org before clearing\n let previousOrgId: string | null = null;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT client_org_id FROM agents WHERE id = $1`, [agentId]);\n previousOrgId = rows[0]?.client_org_id || null;\n } else {\n const row = await db.getEngineDB()!.get(`SELECT client_org_id FROM agents WHERE id = ?`, [agentId]);\n previousOrgId = (row as any)?.client_org_id || null;\n }\n\n // Update admin agents table\n if (isPostgres) {\n await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);\n } else {\n await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);\n }\n // Also update engine managed_agents table\n const engineDb = db.getEngineDB();\n if (engineDb) {\n try {\n if (isPostgres) {\n await (db as any)._query(`UPDATE managed_agents SET client_org_id = NULL, updated_at = NOW() WHERE id = $1`, [agentId]);\n } else {\n await engineDb.run(`UPDATE managed_agents SET client_org_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [agentId]);\n }\n } catch { /* column may not exist yet before migration */ }\n }\n\n // Clear org-inherited credentials from DB\n let credentialsCleared = 0;\n if (previousOrgId) {\n try {\n if (isPostgres) {\n await (db as any)._query(\n `UPDATE managed_agents SET config = config - 'emailConfig' - 'email', updated_at = NOW() WHERE id = $1`,\n [agentId]\n ).catch(() => {});\n } else {\n const row = await db.getEngineDB()!.get(`SELECT config FROM managed_agents WHERE id = ?`, [agentId]);\n if (row) {\n const cfg = JSON.parse((row as any).config || '{}');\n delete cfg.emailConfig;\n delete cfg.email;\n await db.getEngineDB()!.run(`UPDATE managed_agents SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [JSON.stringify(cfg), agentId]);\n }\n }\n if ((globalThis as any).__vault) {\n const vault = (globalThis as any).__vault;\n try {\n const secrets = await vault.getSecretsByOrg(previousOrgId, 'skill_credential');\n for (const secret of secrets) {\n if (secret.name?.includes(':agent:' + agentId)) {\n await vault.deleteSecret(secret.id);\n credentialsCleared++;\n }\n }\n } catch { /* best effort */ }\n }\n } catch { /* best effort */ }\n }\n\n // Clear ALL credentials from the running agent (in-memory)\n try {\n const oi = (globalThis as any).__orgIntegrations;\n if (oi) {\n const agent = oi.lifecycle?.getAgent?.(agentId);\n if (agent?.config) {\n agent.config.emailConfig = null;\n if (agent.config.email) agent.config.email = null;\n }\n if (agent) {\n agent.client_org_id = null;\n agent.clientOrgId = null;\n }\n }\n } catch { /* best effort */ }\n\n return c.json({ success: true, previousOrgId, credentialsCleared });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Agent Knowledge Access ─────────────────────────────\n\n api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n return c.json({ grants: rows });\n } else {\n const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n return c.json({ grants: rows });\n }\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {\n const agentId = c.req.param('id');\n const { grants } = await c.req.json();\n if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await (db as any)._query(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n } else {\n const engineDb = db.getEngineDB()!;\n await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);\n for (const g of grants) {\n const id = (await import('crypto')).randomUUID();\n await engineDb.run(\n `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,\n [id, agentId, g.knowledgeBaseId, g.accessType || 'read']\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing ────────────────────────────\n\n api.get('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const months = parseInt(c.req.query('months') || '12');\n try {\n const isPostgres = (db as any).pool;\n let records: any[];\n if (isPostgres) {\n const { rows } = await (db as any)._query(\n `SELECT * FROM org_billing_records WHERE org_id = $1 ORDER BY month DESC LIMIT $2`,\n [orgId, months]\n );\n records = rows;\n } else {\n records = await db.getEngineDB()!.all(\n `SELECT * FROM org_billing_records WHERE org_id = ? ORDER BY month DESC LIMIT ?`,\n [orgId, months]\n );\n }\n return c.json({ records });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.put('/organizations/:id/billing', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n const { records } = await c.req.json();\n if (!Array.isArray(records)) return c.json({ error: 'records must be an array' }, 400);\n try {\n const isPostgres = (db as any).pool;\n const { randomUUID } = await import('crypto');\n for (const r of records) {\n if (!r.month) continue;\n if (isPostgres) {\n await (db as any)._query(\n `INSERT INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (org_id, agent_id, month) DO UPDATE SET\n revenue = EXCLUDED.revenue, token_cost = EXCLUDED.token_cost,\n input_tokens = EXCLUDED.input_tokens, output_tokens = EXCLUDED.output_tokens,\n notes = EXCLUDED.notes, updated_at = NOW()`,\n [randomUUID(), orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n } else {\n const id = randomUUID();\n await db.getEngineDB()!.run(\n `INSERT OR REPLACE INTO org_billing_records (id, org_id, agent_id, month, revenue, token_cost, input_tokens, output_tokens, notes)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, orgId, r.agentId || null, r.month, r.revenue || 0, r.tokenCost || 0, r.inputTokens || 0, r.outputTokens || 0, r.notes || null]\n );\n }\n }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Organization Billing Summary ─────────────────────\n\n api.get('/organizations/:id/billing-summary', requireRole('admin'), async (c) => {\n const orgId = c.req.param('id');\n try {\n const isPostgres = (db as any).pool;\n let rows: any[];\n if (isPostgres) {\n const result = await (db as any)._query(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = $1\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n rows = result.rows;\n } else {\n rows = await db.getEngineDB()!.all(\n `SELECT month, SUM(revenue) as total_revenue, SUM(token_cost) as total_cost,\n SUM(input_tokens) as total_input_tokens, SUM(output_tokens) as total_output_tokens\n FROM org_billing_records WHERE org_id = ?\n GROUP BY month ORDER BY month ASC`, [orgId]\n );\n }\n return c.json({ summary: rows });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n /** Stop and optionally delete tunnel */\n api.post('/tunnel/stop', requireRole('admin'), async (c) => {\n try {\n const { execSync } = await import('child_process');\n try { execSync('pm2 stop cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n try { execSync('pm2 delete cloudflared 2>/dev/null', { timeout: 5000 }); } catch { /* ok */ }\n return c.json({ success: true });\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ─── Custom Agent Roles (Soul Templates) ──────────────\n\n const rolesQuery = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.all(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesExec = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n await (db as any)._query(sql, params);\n } else {\n const engineDb = db.getEngineDB();\n await engineDb!.run(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const rolesGet = async (sql: string, params: any[] = []) => {\n const isPostgres = (db as any).pool;\n if (isPostgres) {\n const { rows } = await (db as any)._query(sql, params);\n return rows[0] || null;\n } else {\n const engineDb = db.getEngineDB();\n return await engineDb!.get(sql.replace(/\\$(\\d+)/g, '?'), params);\n }\n };\n\n const mapRole = (r: any) => {\n if (!r) return null;\n const parse = (v: any) => { try { return typeof v === 'string' ? JSON.parse(v) : v; } catch { return v; } };\n return {\n id: r.id, name: r.name, slug: r.slug, category: r.category || 'operations',\n description: r.description, personality: r.personality || '',\n identity: parse(r.identity) || {}, suggestedSkills: parse(r.suggested_skills) || [],\n suggestedPreset: r.suggested_preset || null, tags: parse(r.tags) || [],\n orgId: r.org_id, isActive: r.is_active !== false && r.is_active !== 0,\n isCustom: true, metadata: parse(r.metadata),\n createdBy: r.created_by, createdAt: r.created_at, updatedAt: r.updated_at,\n };\n };\n\n // List custom agent roles\n api.get('/roles', requireRole('admin'), async (c) => {\n try {\n const orgId = c.req.query('orgId');\n let sql: string, params: any[];\n if (orgId) {\n sql = 'SELECT * FROM custom_roles WHERE (org_id = $1 OR org_id IS NULL) AND is_active = $2 ORDER BY category, name';\n params = [orgId, (db as any).pool ? true : 1];\n } else {\n sql = 'SELECT * FROM custom_roles ORDER BY category, name';\n params = [];\n }\n const rows = await rolesQuery(sql, params);\n return c.json({ roles: rows.map(mapRole) });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get single role\n api.get('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const role = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!role) return c.json({ error: 'Role not found' }, 404);\n return c.json(role);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create role\n api.post('/roles', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n validate(body, [\n { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },\n { field: 'category', type: 'string', required: true },\n { field: 'description', type: 'string', maxLength: 1024 },\n ]);\n const slug = (body.slug || body.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const existing = body.orgId\n ? await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id = $2', [slug, body.orgId])\n : await rolesGet('SELECT id FROM custom_roles WHERE slug = $1 AND org_id IS NULL', [slug]);\n if (existing) return c.json({ error: 'A role with this name already exists' }, 409);\n\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, body.name, slug, body.category || 'operations', body.description || null,\n body.personality || null, JSON.stringify(body.identity || {}),\n JSON.stringify(body.suggestedSkills || []), body.suggestedPreset || null,\n JSON.stringify(body.tags || []), body.orgId || null,\n isPostgres ? true : 1, JSON.stringify(body.metadata || {}),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, e instanceof ValidationError ? 400 : 500); }\n });\n\n // Update role\n api.put('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json();\n const isPostgres = (db as any).pool;\n const fields: string[] = [];\n const values: any[] = [];\n let i = 1;\n const strMap: Record = { name: 'name', category: 'category', description: 'description', personality: 'personality', suggestedPreset: 'suggested_preset' };\n for (const [key, col] of Object.entries(strMap)) {\n if (body[key] !== undefined) { fields.push(`${col} = $${i++}`); values.push(body[key]); }\n }\n if (body.identity !== undefined) { fields.push(`identity = $${i++}`); values.push(JSON.stringify(body.identity)); }\n if (body.suggestedSkills !== undefined) { fields.push(`suggested_skills = $${i++}`); values.push(JSON.stringify(body.suggestedSkills)); }\n if (body.tags !== undefined) { fields.push(`tags = $${i++}`); values.push(JSON.stringify(body.tags)); }\n if (body.orgId !== undefined) { fields.push(`org_id = $${i++}`); values.push(body.orgId || null); }\n if (body.isActive !== undefined) { fields.push(`is_active = $${i++}`); values.push(isPostgres ? !!body.isActive : (body.isActive ? 1 : 0)); }\n if (body.metadata !== undefined) { fields.push(`metadata = $${i++}`); values.push(JSON.stringify(body.metadata)); }\n if (body.name && body.name !== existing.name) {\n const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n fields.push(`slug = $${i++}`); values.push(slug);\n }\n if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);\n fields.push(`updated_at = $${i++}`); values.push(new Date().toISOString());\n values.push(c.req.param('id'));\n await rolesExec(`UPDATE custom_roles SET ${fields.join(', ')} WHERE id = $${i}`, values);\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')])));\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete role\n api.delete('/roles/:id', requireRole('admin'), async (c) => {\n try {\n const existing = await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n if (!existing) return c.json({ error: 'Role not found' }, 404);\n await rolesExec('DELETE FROM custom_roles WHERE id = $1', [c.req.param('id')]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Duplicate role\n api.post('/roles/:id/duplicate', requireRole('admin'), async (c) => {\n try {\n const source = mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [c.req.param('id')]));\n if (!source) return c.json({ error: 'Role not found' }, 404);\n const body = await c.req.json().catch(() => ({}));\n const newName = body.name || source.name + ' (Copy)';\n const slug = newName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n const id = crypto.randomUUID();\n const isPostgres = (db as any).pool;\n await rolesExec(\n `INSERT INTO custom_roles (id, name, slug, category, description, personality, identity, suggested_skills, suggested_preset, tags, org_id, is_active, metadata, created_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,\n [\n id, newName, slug, source.category, source.description, source.personality,\n JSON.stringify(source.identity), JSON.stringify(source.suggestedSkills),\n source.suggestedPreset, JSON.stringify(source.tags), source.orgId || null,\n isPostgres ? true : 1, JSON.stringify(source.metadata),\n (c as any).get?.('userId') || 'system',\n ]\n );\n return c.json(mapRole(await rolesGet('SELECT * FROM custom_roles WHERE id = $1', [id])), 201);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── System Update Endpoints ─────────────────────────\n \n api.get('/system/update-check', async (c) => {\n try {\n const { checkForUpdate, getCachedUpdateCheck } = await import('../cli-update.js');\n // Return cached if checked within last 5 minutes\n const cached = getCachedUpdateCheck();\n if (cached && Date.now() - new Date(cached.checkedAt).getTime() < 5 * 60_000) {\n return c.json(cached);\n }\n const info = await checkForUpdate();\n return c.json(info);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n api.post('/system/update', async (c) => {\n try {\n const { performUpdate } = await import('../cli-update.js');\n // Run update in background — response returns immediately\n const result = await performUpdate({ restart: true });\n return c.json(result);\n } catch (e: any) {\n return c.json({ error: e.message }, 500);\n }\n });\n\n // ═══ POLYMARKET TRADING MANAGEMENT ═══════════════════════════\n\n // Helper: get engine DB for raw SQL queries (works across SQLite, Postgres, MySQL)\n const edb = () => db.getEngineDB();\n\n // Get trading config for an agent\n // ─── SOCKS Proxy Management ─────────────────────────────────\n // Ensure polymarket proxy table exists\n let _polyTablesInit = false;\n async function ensurePolyDB() {\n if (_polyTablesInit) return;\n const e = edb();\n if (!e) return;\n try {\n // initPolymarketDB expects db.execute(); engine DB has run/get/all — shim it\n const dbShim = e.execute ? e : Object.assign({}, e, {\n execute: e.run || e.execute,\n query: e.all || e.query,\n });\n await initPolymarketDB(dbShim);\n\n // Ensure poly_goals table exists (not part of initPolymarketDB)\n await e.run(`CREATE TABLE IF NOT EXISTS poly_goals (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n period TEXT NOT NULL DEFAULT 'daily',\n target_value REAL NOT NULL,\n current_value REAL DEFAULT 0,\n met INTEGER DEFAULT 0,\n met_at TEXT,\n streak INTEGER DEFAULT 0,\n best_streak INTEGER DEFAULT 0,\n times_met INTEGER DEFAULT 0,\n times_missed INTEGER DEFAULT 0,\n last_evaluated TEXT,\n enabled INTEGER DEFAULT 1,\n notify_on_met INTEGER DEFAULT 1,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n\n _polyTablesInit = true;\n } catch (err: any) {\n console.error('[polymarket] Failed to init tables:', err.message);\n }\n }\n\n // Eagerly init polymarket tables so they exist before any route is hit\n ensurePolyDB().catch((e: any) => console.warn('[polymarket] Eager init:', e.message));\n\n // Middleware: ensure poly tables exist for ALL /polymarket/* routes\n api.use('/polymarket/*', async (_c, next) => { await ensurePolyDB(); return next(); });\n\n api.get('/polymarket/proxy/status', requireRole('admin'), async (c) => {\n await autoConnectProxy(edb());\n const state = getProxyState();\n const config = await loadProxyConfig(edb());\n return c.json({\n ...state,\n configured: !!config,\n config: config ? { proxyMode: config.proxyMode, proxyUrl: config.proxyUrl, vpsHost: config.vpsHost, vpsUser: config.vpsUser, vpsPort: config.vpsPort, socksPort: config.socksPort, authMethod: config.authMethod, enabled: config.enabled } : null,\n });\n });\n\n api.post('/polymarket/proxy/config', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n await saveProxyConfig(edb(), {\n enabled: body.enabled ?? false,\n proxyMode: body.proxyMode || 'http',\n proxyUrl: body.proxyUrl || undefined,\n proxyToken: body.proxyToken || undefined,\n vpsHost: body.vpsHost || '',\n vpsUser: body.vpsUser || 'root',\n vpsPort: body.vpsPort || 22,\n socksPort: body.socksPort || 1080,\n authMethod: body.authMethod || 'password',\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n password: body.password || undefined,\n });\n return c.json({ status: 'saved' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/connect', requireRole('owner'), async (c) => {\n try {\n const state = await startProxy(edb());\n return c.json(state);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/proxy/disconnect', requireRole('owner'), async (c) => {\n await stopProxy();\n return c.json({ status: 'disconnected' });\n });\n\n api.post('/polymarket/proxy/test', requireRole('admin'), async (c) => {\n const state = getProxyState();\n if (!state.connected) return c.json({ error: 'Proxy not connected' }, 400);\n try {\n const config = await loadProxyConfig(edb());\n if (config?.proxyMode === 'http' && config.proxyUrl) {\n // Test HTTP proxy by hitting /time through it\n const headers: Record = {};\n if (config.proxyToken) headers['x-proxy-token'] = config.proxyToken;\n const res = await fetch(config.proxyUrl.replace(/\\/$/, '') + '/time', { headers, signal: AbortSignal.timeout(10000) });\n const time = await res.text();\n return c.json({ status: 'ok', clobTime: time, geoblock: { blocked: false } });\n }\n // SOCKS tunnel test\n const { SocksProxyAgent } = await import('socks-proxy-agent');\n const agent = new SocksProxyAgent(`socks5://127.0.0.1:${state.socksPort}`);\n const res = await fetch('https://clob.polymarket.com/time', { agent } as any);\n const time = await res.text();\n const geoRes = await fetch('https://polymarket.com/api/geoblock', { agent } as any);\n const geo = await geoRes.json();\n return c.json({ status: 'ok', clobTime: time, geoblock: geo });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-deploy proxy to VPS\n api.post('/polymarket/proxy/setup', requireRole('owner'), async (c) => {\n try {\n const body = await c.req.json();\n if (!body.host) return c.json({ error: 'Server address is required' }, 400);\n\n const result = await deployProxyToVPS({\n host: body.host,\n user: body.user || 'root',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n port: body.sshPort || 22,\n });\n\n if (result.success) {\n // Auto-save the proxy config\n await saveProxyConfig(edb(), {\n enabled: true,\n proxyMode: 'http',\n proxyUrl: result.proxyUrl,\n proxyToken: result.proxyToken,\n vpsHost: body.host,\n vpsUser: body.user || 'root',\n vpsPort: body.sshPort || 22,\n authMethod: body.password ? 'password' : 'key',\n password: body.password || undefined,\n sshKeyPath: body.sshKeyPath || undefined,\n sshKeyContent: body.sshKeyContent || undefined,\n });\n }\n\n return c.json(result);\n } catch (e: any) { return c.json({ error: e.message, logs: [] }, 500); }\n });\n\n api.get('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT * FROM poly_trading_config WHERE agent_id = ?`, [agentId]);\n return c.json({ config: row || null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update trading config\n api.put('/polymarket/:agentId/config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const fields = ['mode', 'max_position_size', 'max_order_size', 'max_total_exposure', 'max_daily_trades',\n 'max_daily_loss', 'max_drawdown_pct', 'stop_loss_pct', 'take_profit_pct', 'cash_reserve_pct'];\n const sets: string[] = [];\n const vals: any[] = [];\n for (const f of fields) {\n if (body[f] !== undefined) { sets.push(`${f} = ?`); vals.push(body[f]); }\n }\n if (sets.length === 0) return c.json({ error: 'No fields to update' }, 400);\n sets.push('updated_at = CURRENT_TIMESTAMP');\n const changedFields = fields.filter(f => body[f] !== undefined);\n const insertPlaceholders = changedFields.map(() => '?').join(',');\n // Params: [agentId, ...insertVals, ...updateVals, agentId_for_fallback]\n // For the upsert: INSERT needs agentId + vals, ON CONFLICT SET needs vals again\n await edb()?.run(`\n INSERT INTO poly_trading_config (agent_id, ${changedFields.join(',')}, updated_at)\n VALUES (?, ${insertPlaceholders}, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET ${sets.join(', ')}\n `, [agentId, ...vals, ...vals]).catch(() =>\n edb()?.run(`UPDATE poly_trading_config SET ${sets.join(', ')} WHERE agent_id = ?`, [...vals, agentId])\n );\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get pending trades for an agent\n api.get('/polymarket/:agentId/pending', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending' ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Approve or reject a pending trade\n api.post('/polymarket/trades/:tradeId/decide', requireRole('admin'), async (c) => {\n try {\n const tradeId = c.req.param('tradeId');\n const { decision, reason } = await c.req.json();\n\n if (decision === 'approve') {\n // Load trade BEFORE resolving\n const trade = await edb()?.get(`SELECT * FROM poly_pending_trades WHERE id = ? AND status = 'pending'`, [tradeId]) as any;\n if (!trade) return c.json({ error: 'Trade not found or already resolved' }, 404);\n\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'approved', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n\n // Execute the trade on-chain\n try {\n const db = { execute: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params), query: (sql: string, params: any[]) => edb()?.all(sql.replace(/\\$(\\d+)/g, '?'), params) };\n const result = await executeOrder(trade.agent_id, db, tradeId, {\n token_id: trade.token_id, side: trade.side,\n price: trade.price, size: trade.size,\n order_type: trade.order_type, tick_size: trade.tick_size,\n neg_risk: trade.neg_risk, market_question: trade.market_question,\n outcome: trade.outcome, rationale: trade.rationale,\n }, 'admin_approved');\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n return c.json({ status: 'ok', decision: 'approve', execution: parsed });\n } catch (execErr: any) {\n return c.json({ status: 'ok', decision: 'approve', execution_error: execErr.message });\n }\n } else {\n await edb()?.run(`UPDATE poly_pending_trades SET status = 'rejected', resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?`,\n [reason || 'admin', tradeId]);\n return c.json({ status: 'ok', decision: 'reject' });\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get trade history for an agent\n api.get('/polymarket/:agentId/trades', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const limit = parseInt(c.req.query('limit') || '50');\n const rows = await edb()?.all(`SELECT * FROM poly_trade_log WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];\n return c.json({ trades: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get wallet status (address only, never expose private key)\n api.get('/polymarket/:agentId/wallet', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT agent_id, funder_address, signature_type, created_at, updated_at FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n return c.json({ wallet: row ? { address: row.funder_address, signatureType: row.signature_type, connected: true, createdAt: row.created_at } : null });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Generate a new wallet (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/create', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n\n // Check if wallet already exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing?.funder_address) return c.json({ error: 'Wallet already exists at ' + existing.funder_address + '. Delete it first or use import to replace.' }, 400);\n\n // Generate wallet using ethersproject\n let address: string, privateKey: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = Wallet.createRandom();\n address = wallet.address;\n privateKey = wallet.privateKey;\n } catch {\n return c.json({ error: 'SDK not installed. Run the agent once to auto-install.' }, 500);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n `, [agentId, encrypt(privateKey), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.created', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, privateKey });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import API credentials only (OWNER ONLY) — for existing Polymarket users\n api.post('/polymarket/:agentId/wallet/import-api-creds', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.api_key || !body.api_secret || !body.api_passphrase) return c.json({ error: 'api_key, api_secret, and api_passphrase are required' }, 400);\n\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n const encrypt = (val: string) => { try { return vault ? vault.encrypt(val) : val; } catch { return val; } };\n\n // Check if wallet row exists\n const existing = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (existing) {\n // Update API creds on existing wallet\n await edb()?.run(`UPDATE poly_wallet_credentials SET api_key = ?, api_secret = ?, api_passphrase = ?, updated_at = CURRENT_TIMESTAMP WHERE agent_id = ?`,\n [encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), agentId]);\n } else {\n // Create new row with just API creds (no private key)\n await edb()?.run(`INSERT INTO poly_wallet_credentials (agent_id, api_key, api_secret, api_passphrase, funder_address, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,\n [agentId, encrypt(body.api_key), encrypt(body.api_secret), encrypt(body.api_passphrase), body.wallet_address || '']);\n }\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.api_creds_imported', resourceType: 'agent', resourceId: agentId, details: { hasKey: true }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', message: 'API credentials imported. Agent can now trade using your existing Polymarket account.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Import wallet private key (OWNER ONLY)\n api.post('/polymarket/:agentId/wallet/import', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.private_key) return c.json({ error: 'private_key is required' }, 400);\n\n const pk = body.private_key.startsWith('0x') ? body.private_key : `0x${body.private_key}`;\n\n // Validate the private key by deriving the address\n let address: string;\n try {\n const { createRequire } = await import('module');\n const sdkDir = (await import('path')).join((await import('os')).homedir(), '.agenticmail/polymarket-sdk');\n const req = createRequire(sdkDir + '/node_modules/.package.json');\n const { Wallet } = req('@ethersproject/wallet');\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n // Fallback: try ethers from enterprise node_modules\n try {\n const { Wallet } = await import('ethers' as any);\n const wallet = new Wallet(pk);\n address = wallet.address;\n } catch {\n return c.json({ error: 'Invalid private key or SDK not installed' }, 400);\n }\n }\n\n // Encrypt and store\n const { SecureVault } = await import('../engine/vault.js');\n let vault: any;\n try { vault = new SecureVault(); } catch {}\n\n const encrypt = (val: string) => {\n if (!val) return val;\n try { return vault ? vault.encrypt(val) : val; } catch { return val; }\n };\n\n await edb()?.run(`\n INSERT INTO poly_wallet_credentials (agent_id, private_key_encrypted, funder_address, signature_type, updated_at)\n VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET\n private_key_encrypted = ?, funder_address = ?, signature_type = 0, api_key = NULL, api_secret = NULL, api_passphrase = NULL, updated_at = CURRENT_TIMESTAMP\n `, [agentId, encrypt(pk), address, encrypt(pk), address]);\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.imported', resourceType: 'agent', resourceId: agentId, details: { address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({ status: 'ok', address, message: 'Wallet imported successfully. API keys will be derived automatically on first use.' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Export wallet private key (OWNER ONLY — requires password confirmation)\n api.post('/polymarket/:agentId/wallet/export', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.confirm || body.confirm !== 'EXPORT') {\n return c.json({ error: 'Must send { confirm: \"EXPORT\" } to confirm private key export' }, 400);\n }\n const row = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row) return c.json({ error: 'No wallet found for this agent' }, 404);\n\n // Audit log this sensitive action\n try {\n await (db as any).createAuditLog({\n userId: c.get('userId' as any),\n action: 'wallet.private_key_exported',\n resourceType: 'agent',\n resourceId: agentId,\n details: { address: row.funder_address },\n ipAddress: c.req.header('x-forwarded-for') || 'unknown',\n });\n } catch {}\n\n return c.json({\n address: row.funder_address,\n privateKey: (() => { try { return vault.decrypt(row.private_key_encrypted); } catch { return row.private_key_encrypted; } })(),\n warning: 'This private key controls all funds in this wallet. Store it securely. Anyone with this key can drain the wallet.',\n importInstructions: {\n metamask: '1. Open MetaMask → Import Account → Paste private key',\n rabby: '1. Open Rabby → Add Address → Import Private Key → Paste',\n polymarket: 'Use this key to sign into polymarket.com via wallet connect',\n }\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Whitelisted Withdrawal Addresses ──────────────────────\n api.get('/polymarket/:agentId/wallet/whitelist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_whitelisted_addresses WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ addresses: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/whitelist', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n if (!body.label || !body.address) return c.json({ error: 'label and address are required' }, 400);\n // Validate Ethereum address format\n const addr = body.address.trim();\n if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return c.json({ error: 'Invalid Ethereum address format. Must be 0x followed by 40 hex characters.' }, 400);\n // Check for duplicate\n const existing = await edb()?.get(`SELECT id FROM poly_whitelisted_addresses WHERE agent_id = ? AND address = ?`, [agentId, addr.toLowerCase()]);\n if (existing) return c.json({ error: 'This address is already whitelisted' }, 409);\n // Configurable cooling period (default 24h, min 0)\n const coolingHours = Math.max(0, body.cooling_hours != null ? body.cooling_hours : 24);\n const coolingUntil = new Date(Date.now() + coolingHours * 3600000).toISOString();\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_whitelisted_addresses (id, agent_id, label, address, added_by, per_tx_limit, daily_limit, cooling_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.label.trim(), addr.toLowerCase(), c.get('userId' as any) || 'unknown', body.per_tx_limit || 100, body.daily_limit || 500, coolingUntil]\n );\n // Audit log\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_added', resourceType: 'agent', resourceId: agentId, details: { label: body.label, address: addr, coolingUntil }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, id, coolingUntil, coolingHours, message: (coolingHours > 0 ? coolingHours + '-hour cooling period active. Transfers to this address will be blocked until ' + coolingUntil : 'Address added with no cooling period.') });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n // Check for pending transfers first\n const pending = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_transfer_requests WHERE whitelist_id = ? AND status = 'pending'`, [addrId]);\n if (pending?.cnt > 0) return c.json({ error: 'Cannot remove: there are pending transfers to this address. Reject them first.' }, 400);\n const row = await edb()?.get(`SELECT label, address FROM poly_whitelisted_addresses WHERE id = ?`, [addrId]);\n await edb()?.run(`DELETE FROM poly_whitelisted_addresses WHERE id = ? AND agent_id = ?`, [addrId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_removed', resourceType: 'agent', resourceId: agentId, details: { label: row?.label, address: row?.address }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.put('/polymarket/:agentId/wallet/whitelist/:addrId', requireRole('owner'), async (c) => {\n try {\n const addrId = c.req.param('addrId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const updates: string[] = []; const vals: any[] = [];\n if (body.label) { updates.push('label = ?'); vals.push(body.label); }\n if (body.per_tx_limit != null) { updates.push('per_tx_limit = ?'); vals.push(body.per_tx_limit); }\n if (body.daily_limit != null) { updates.push('daily_limit = ?'); vals.push(body.daily_limit); }\n if (body.is_active != null) { updates.push('is_active = ?'); vals.push(body.is_active ? 1 : 0); }\n if (updates.length === 0) return c.json({ error: 'No fields to update' }, 400);\n vals.push(addrId, agentId);\n await edb()?.run(`UPDATE poly_whitelisted_addresses SET ${updates.join(', ')} WHERE id = ? AND agent_id = ?`, vals);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.whitelist_updated', resourceType: 'agent', resourceId: agentId, details: body, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Transfer Requests ──────────────────────\n api.get('/polymarket/:agentId/transfers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_transfer_requests WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ transfers: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/approve', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const txId = c.req.param('txId');\n const tx = await edb()?.get(`SELECT * FROM poly_transfer_requests WHERE id = ? AND agent_id = ?`, [txId, agentId]) as any;\n if (!tx) return c.json({ error: 'Transfer not found' }, 404);\n if (tx.status !== 'pending') return c.json({ error: 'Transfer is not pending (status: ' + tx.status + ')' }, 400);\n if (tx.expires_at < new Date().toISOString()) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'expired', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]);\n return c.json({ error: 'Transfer has expired' }, 400);\n }\n\n // Execute on-chain transfer\n try {\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds) { await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = 'No wallet credentials', resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [txId]); return c.json({ error: 'No wallet configured' }, 400); }\n\n // Use ethers to send transaction\n let txHash = '';\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n try {\n const { Wallet, JsonRpcProvider } = await importSDK('ethers');\n const provider = new JsonRpcProvider('https://polygon.drpc.org');\n const wallet = new Wallet(decryptedKey, provider);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet.sendTransaction({ to: tx.to_address, value: BigInt(Math.floor(tx.amount * 1e18)).toString() });\n txHash = sendTx.hash;\n } else {\n // USDC transfer (ERC-20)\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const iface = new (await importSDK('ethers')).Interface(['function transfer(address to, uint256 amount) returns (bool)']);\n const data = iface.encodeFunctionData('transfer', [tx.to_address, BigInt(Math.floor(tx.amount * 1e6)).toString()]);\n const sendTx = await wallet.sendTransaction({ to: USDC_POLYGON, data });\n txHash = sendTx.hash;\n }\n } catch (ethersErr: any) {\n // Fallback: try ethers v5\n try {\n const { Wallet: W5 } = await importSDK('@ethersproject/wallet');\n const { JsonRpcProvider: P5 } = await importSDK('@ethersproject/providers');\n const { Contract: C5 } = await importSDK('@ethersproject/contracts');\n const provider5 = new P5('https://polygon.drpc.org');\n const wallet5 = new W5(decryptedKey, provider5);\n if (tx.token === 'MATIC') {\n const sendTx = await wallet5.sendTransaction({ to: tx.to_address, value: (BigInt(Math.floor(tx.amount * 1e18))).toString() });\n txHash = sendTx.hash;\n } else {\n const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const usdc = new C5(USDC_POLYGON, ['function transfer(address to, uint256 amount) returns (bool)'], wallet5);\n const sendTx = await usdc.transfer(tx.to_address, (BigInt(Math.floor(tx.amount * 1e6))).toString());\n txHash = sendTx.hash;\n }\n } catch (v5Err: any) {\n throw new Error(`Transfer execution failed: ${ethersErr.message}. Fallback: ${v5Err.message}`);\n }\n }\n\n // Update transfer record\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'completed', approved_by = ?, tx_hash = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [c.get('userId' as any), txHash, txId]);\n // Update daily tracking\n const today = new Date().toISOString().slice(0, 10);\n await edb()?.run(\n `INSERT INTO poly_transfer_daily (agent_id, address, date, total_transferred, tx_count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(agent_id, address, date) DO UPDATE SET total_transferred = total_transferred + ?, tx_count = tx_count + 1`,\n [agentId, tx.to_address, today, tx.amount, tx.amount]\n );\n // Audit\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_approved', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, token: tx.token, to: tx.to_address, label: tx.to_label, txHash }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true, txHash, status: 'completed' });\n } catch (execErr: any) {\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'failed', error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, [execErr.message, txId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_failed', resourceType: 'agent', resourceId: agentId, details: { amount: tx.amount, error: execErr.message }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ error: 'Transfer execution failed: ' + execErr.message }, 500);\n }\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/transfers/:txId/reject', requireRole('owner'), async (c) => {\n try {\n const txId = c.req.param('txId');\n const agentId = c.req.param('agentId');\n const body = await c.req.json().catch(() => ({}));\n await edb()?.run(`UPDATE poly_transfer_requests SET status = 'rejected', approved_by = ?, error = ?, resolved_at = CURRENT_TIMESTAMP WHERE id = ? AND agent_id = ?`,\n [c.get('userId' as any), body.reason || 'Rejected by owner', txId, agentId]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_rejected', resourceType: 'agent', resourceId: agentId, details: { txId, reason: body.reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Wallet Security: 2FA/PIN for transfers ──────────────\n api.get('/polymarket/:agentId/wallet/security-status', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const userId = c.get('userId' as any);\n // Check if user has 2FA enabled\n const user = await db.getUser(userId);\n const has2fa = !!(user as any)?.totpEnabled;\n // Check if wallet PIN exists for this agent\n const pinRow = await edb()?.get(`SELECT id FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n const hasPin = !!pinRow;\n return c.json({ has2fa, hasPin });\n } catch (e: any) { return c.json({ has2fa: false, hasPin: false }); }\n });\n\n api.post('/polymarket/:agentId/wallet/setup-pin', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n // Sanitize: extract pin, coerce to string, strip non-digits\n const rawPin = String(body?.pin ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!rawPin || rawPin.length !== 6) return c.json({ error: 'PIN must be exactly 6 digits' }, 400);\n // Reject trivial PINs\n if (/^(.)\\1{5}$/.test(rawPin) || rawPin === '123456' || rawPin === '654321') {\n return c.json({ error: 'PIN is too simple. Choose a less predictable 6-digit PIN.' }, 400);\n }\n // Encrypt PIN using the shared vault instance (same key used for wallet credentials)\n const crypto = await import('node:crypto');\n const encryptedPin = vault.encrypt(rawPin);\n // Ensure table exists (cross-DB compatible)\n await edb()?.run(`CREATE TABLE IF NOT EXISTS poly_wallet_pins (\n id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, encrypted_pin TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP\n )`);\n // Upsert — ON CONFLICT works on both SQLite 3.24+ and Postgres\n const id = crypto.randomUUID();\n await edb()?.run(\n `INSERT INTO poly_wallet_pins (id, agent_id, encrypted_pin, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT (agent_id) DO UPDATE SET encrypted_pin = ?, updated_at = CURRENT_TIMESTAMP`,\n [id, agentId, encryptedPin, encryptedPin]);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_setup', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/verify-transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const method = String(body?.method ?? '');\n const code = String(body?.code ?? '').replace(/\\D/g, '').slice(0, 6);\n if (!code || code.length !== 6) return c.json({ ok: false, error: 'Code must be exactly 6 digits' }, 400);\n if (method === '2fa') {\n const userId = c.get('userId' as any);\n const user = await db.getUser(userId) as any;\n if (!user?.totpSecret) return c.json({ ok: false, error: '2FA not set up' }, 400);\n // Verify TOTP\n const crypto = await import('node:crypto');\n const epoch = Math.floor(Date.now() / 30000);\n let valid = false;\n for (let i = -1; i <= 1; i++) {\n const counter = epoch + i;\n const buf = Buffer.alloc(8);\n buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);\n buf.writeUInt32BE(counter & 0xFFFFFFFF, 4);\n let secret = user.totpSecret;\n // Decode base32\n const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n let bits = '', bytes: number[] = [];\n for (const ch of secret.toUpperCase().replace(/=+$/, '')) {\n const val = base32chars.indexOf(ch);\n if (val >= 0) bits += val.toString(2).padStart(5, '0');\n }\n for (let b = 0; b + 8 <= bits.length; b += 8) bytes.push(parseInt(bits.slice(b, b + 8), 2));\n const keyBuf = Buffer.from(bytes);\n const hmac = crypto.createHmac('sha1', keyBuf).update(buf).digest();\n const offset = hmac[hmac.length - 1] & 0xf;\n const otp = ((hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]) % 1000000;\n if (otp.toString().padStart(6, '0') === code) { valid = true; break; }\n }\n if (!valid) return c.json({ ok: false, error: 'Invalid 2FA code' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.2fa_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n } else if (method === 'pin') {\n const pinRow = await edb()?.get(`SELECT encrypted_pin FROM poly_wallet_pins WHERE agent_id = ?`, [agentId]) as any;\n if (!pinRow) return c.json({ ok: false, error: 'No wallet PIN set up' }, 400);\n // Decrypt and verify using the shared vault instance\n const decrypted = vault.decrypt(pinRow.encrypted_pin);\n if (decrypted !== code) return c.json({ ok: false, error: 'Invalid PIN' }, 401);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.pin_verified', resourceType: 'agent', resourceId: agentId, details: {}, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true });\n }\n return c.json({ ok: false, error: 'Invalid verification method' }, 400);\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/swap', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { direction, amount } = await c.req.json();\n if (!direction || !amount || amount <= 0) return c.json({ error: 'Invalid swap parameters' }, 400);\n\n // Load wallet credentials\n const creds = await edb()?.get(`SELECT private_key_encrypted, funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!creds?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n\n const decryptedKey = (() => { try { return vault.decrypt(creds.private_key_encrypted); } catch { return creds.private_key_encrypted; } })();\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_initiated', resourceType: 'agent', resourceId: agentId, details: { direction, amount }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n // Direct on-chain swap via Uniswap V3 — no agent/LLM needed\n const { Wallet, JsonRpcProvider, Contract } = await import('ethers');\n const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';\n const USDC_BRIDGED = USDC_E_SHARED;\n const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap V3 SwapRouter02\n\n // Connect to Polygon with RPC fallback\n let provider: any = null;\n for (const rpc of ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.llamarpc.com', 'https://polygon-rpc.com']) {\n try { provider = new JsonRpcProvider(rpc); await provider.getNetwork(); break; } catch { provider = null; }\n }\n if (!provider) return c.json({ error: 'Cannot connect to Polygon RPC' }, 502);\n\n const wallet = new Wallet(decryptedKey, provider);\n\n const erc20Abi = [\n 'function balanceOf(address) view returns (uint256)',\n 'function approve(address,uint256) returns (bool)',\n 'function allowance(address,address) view returns (uint256)',\n ];\n const swapRouterAbi = [\n 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)',\n ];\n\n const tokenIn = direction === 'native_to_bridged' ? USDC_NATIVE : USDC_BRIDGED;\n const tokenOut = direction === 'native_to_bridged' ? USDC_BRIDGED : USDC_NATIVE;\n const tokenContract = new Contract(tokenIn, erc20Abi, wallet);\n\n // Check balance\n const balance = await tokenContract.balanceOf(wallet.address);\n if (balance === BigInt(0)) {\n return c.json({ error: direction === 'native_to_bridged' ? 'No native USDC to swap' : 'No USDC.e to swap' }, 400);\n }\n\n let swapAmount = BigInt(Math.floor(amount * 1e6));\n if (swapAmount > balance) swapAmount = balance;\n const swapUSD = (Number(swapAmount) / 1e6).toFixed(2);\n\n // Approve router if needed\n const currentAllowance = await tokenContract.allowance(wallet.address, SWAP_ROUTER);\n if (currentAllowance < swapAmount) {\n const approveTx = await tokenContract.approve(SWAP_ROUTER, '115792089237316195423570985008687907853269984665640564039457584007913129639935');\n await approveTx.wait();\n }\n\n // Execute swap with fee tier fallback (0.01% → 0.05%)\n const router = new Contract(SWAP_ROUTER, swapRouterAbi, wallet);\n let receipt: any;\n let feeTier = '0.01%';\n\n // Helper: wait for tx with RPC retry (Polygon RPCs flake on getTransactionReceipt)\n const waitWithRetry = async (tx: any, retries = 3) => {\n for (let attempt = 0; attempt < retries; attempt++) {\n try { return await tx.wait(); } catch (waitErr: any) {\n if (attempt === retries - 1) throw waitErr;\n // Temporary RPC error — retry with fresh provider\n if (waitErr.code === 'UNKNOWN_ERROR' || waitErr.message?.includes('Temporary internal error')) {\n await new Promise(r => setTimeout(r, 3000 * (attempt + 1)));\n continue;\n }\n throw waitErr;\n }\n }\n };\n\n try {\n const minOut = swapAmount * BigInt(995) / BigInt(1000); // 0.5% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 100, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n } catch (swapErr: any) {\n if (swapErr.message?.includes('revert') || swapErr.message?.includes('STF') || swapErr.message?.includes('Too little received')) {\n const minOut = swapAmount * BigInt(990) / BigInt(1000); // 1% slippage\n const tx = await router.exactInputSingle({\n tokenIn, tokenOut, fee: 500, recipient: wallet.address,\n amountIn: swapAmount, amountOutMinimum: minOut, sqrtPriceLimitX96: BigInt(0),\n });\n receipt = await waitWithRetry(tx);\n feeTier = '0.05%';\n } else {\n throw swapErr;\n }\n }\n\n // Get new balance of output token\n const outContract = new Contract(tokenOut, ['function balanceOf(address) view returns (uint256)'], provider);\n const newBal = await outContract.balanceOf(wallet.address);\n const label = direction === 'native_to_bridged' ? 'Native USDC → USDC.e' : 'USDC.e → Native USDC';\n\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.swap_completed', resourceType: 'agent', resourceId: agentId, details: { direction, amount: swapUSD, txHash: receipt.hash, feeTier }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n\n return c.json({\n ok: true,\n message: `Swapped $${swapUSD} ${label}`,\n txHash: receipt.hash,\n feeTier,\n newBalance: (Number(newBal) / 1e6).toFixed(2),\n });\n } catch (e: any) { return c.json({ error: `Swap failed: ${e.message}` }, 500); }\n });\n\n api.post('/polymarket/:agentId/wallet/transfer', requireRole('owner'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { to_address, amount, token, reason } = await c.req.json();\n if (!to_address || !amount || amount <= 0) return c.json({ error: 'Invalid transfer parameters' }, 400);\n // Verify address is whitelisted\n const wl = await edb()?.get(`SELECT id, label, per_tx_limit, daily_limit FROM poly_withdrawal_addresses WHERE agent_id = ? AND address = ? AND cooling_until < CURRENT_TIMESTAMP`, [agentId, to_address]) as any;\n if (!wl) return c.json({ error: 'Address not whitelisted or still in cooling period' }, 403);\n if (wl.per_tx_limit && amount > wl.per_tx_limit) return c.json({ error: `Exceeds per-transaction limit of $${wl.per_tx_limit}` }, 400);\n // Check daily limit\n const todayTotal = await edb()?.get(`SELECT COALESCE(SUM(amount), 0) as total FROM poly_transfer_requests WHERE agent_id = ? AND to_address = ? AND status = 'completed' AND created_at > datetime('now', '-1 day')`, [agentId, to_address]) as any;\n if (wl.daily_limit && (todayTotal?.total || 0) + amount > wl.daily_limit) return c.json({ error: `Exceeds daily limit of $${wl.daily_limit}` }, 400);\n // Create transfer request\n const crypto = await import('node:crypto');\n const txId = crypto.randomUUID();\n await edb()?.run(`INSERT INTO poly_transfer_requests (id, agent_id, to_address, to_label, amount, token, reason, status, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, datetime('now', '+4 hours'))`,\n [txId, agentId, to_address, wl.label, amount, token || 'USDC.e', reason || '']);\n try { await (db as any).createAuditLog({ userId: c.get('userId' as any), action: 'wallet.transfer_created', resourceType: 'agent', resourceId: agentId, details: { txId, to_address, amount, token, reason }, ipAddress: c.req.header('x-forwarded-for') || 'unknown' }); } catch {}\n return c.json({ ok: true, txId, message: `Transfer of ${amount} ${token || 'USDC.e'} to ${wl.label} submitted. Awaiting execution.` });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Balance cache to prevent flickering when RPC calls fail\n const balanceCache = new Map();\n\n // Get live wallet balance from Polymarket + Polygon\n api.get('/polymarket/:agentId/wallet/balance', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address;\n\n // Parallel fetch: USDC.e (bridged) + USDC (native) + POL balance\n const USDC_E_CONTRACT = USDC_E_SHARED;\n const USDC_NATIVE_CONTRACT = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'; // Native USDC — NOT directly usable on Polymarket\n const POLYGON_RPCS = ['https://polygon.drpc.org', 'https://polygon-bor-rpc.publicnode.com', 'https://polygon.drpc.org', 'https://polygon-rpc.com'];\n let POLYGON_RPC = POLYGON_RPCS[0];\n // Find a working RPC\n for (const rpc of POLYGON_RPCS) {\n try {\n const test = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'eth_blockNumber', params: [] }),\n signal: AbortSignal.timeout(3000) }).then(r => r.json());\n if (test?.result) { POLYGON_RPC = rpc; break; }\n } catch {}\n }\n const addrHex = address.slice(2).toLowerCase();\n const balanceOfData = '0x70a08231000000000000000000000000' + addrHex;\n\n const [usdceRes, usdcNativeRes, maticRes] = await Promise.all([\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', params: [\n { to: USDC_NATIVE_CONTRACT, data: balanceOfData }, 'latest'\n ] })\n }).then(r => r.json()).catch(() => null),\n fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'eth_getBalance', params: [address, 'latest'] })\n }).then(r => r.json()).catch(() => null),\n ]);\n\n const cached = balanceCache.get(address);\n\n let usdceBalance = cached?.usdce ?? 0;\n let usdcNativeBalance = cached?.usdcNative ?? 0;\n let maticBalance = cached?.matic ?? 0;\n\n if (usdceRes?.result && usdceRes.result !== '0x' && !usdceRes.error) {\n usdceBalance = Number(BigInt(usdceRes.result)) / 1e6;\n }\n if (usdcNativeRes?.result && usdcNativeRes.result !== '0x' && !usdcNativeRes.error) {\n usdcNativeBalance = Number(BigInt(usdcNativeRes.result)) / 1e6;\n }\n if (maticRes?.result && maticRes.result !== '0x' && !maticRes.error) {\n maticBalance = Number(BigInt(maticRes.result)) / 1e18;\n }\n\n // Fetch exchange balance (funds deposited to Polymarket exchange for trading)\n // Read creds directly from edb() and create a minimal ClobClient inline —\n // avoids getClobClient's saveWalletCredentials which breaks Postgres $N param reuse\n let exchangeBalance = 0;\n try {\n const e = edb();\n const creds = e ? await e.get(\n `SELECT private_key_encrypted, funder_address, signature_type, api_key, api_secret, api_passphrase FROM poly_wallet_credentials WHERE agent_id = ?`,\n [agentId]\n ) as any : null;\n if (creds?.api_key && creds?.private_key_encrypted) {\n const dec = (v: string) => { try { return vault?.decrypt?.(v) || v; } catch { return v; } };\n const { ensureSDK, importSDK } = await import('../agent-tools/tools/polymarket-runtime.js');\n const sdk = await ensureSDK();\n if (sdk.ready) {\n const { ClobClient } = await importSDK('@polymarket/clob-client');\n const { Wallet } = await importSDK('@ethersproject/wallet');\n const signer = new Wallet(dec(creds.private_key_encrypted));\n const client = new ClobClient(\n 'https://clob.polymarket.com', 137, signer,\n { apiKey: dec(creds.api_key), secret: dec(creds.api_secret), passphrase: dec(creds.api_passphrase) },\n creds.signature_type || 0,\n creds.funder_address || signer.address,\n );\n const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });\n exchangeBalance = Number(bal.balance || 0) / 1e6;\n }\n }\n } catch (exchErr: any) {\n console.warn(`[wallet-balance] Exchange balance fetch failed: ${exchErr?.message}`);\n }\n\n // Cache the good values\n balanceCache.set(address, { usdce: usdceBalance, usdcNative: usdcNativeBalance, usdc: usdceBalance + usdcNativeBalance, matic: maticBalance, ts: Date.now() });\n\n // Get open positions from Polymarket Data API for accurate portfolio values\n const paperPos = await edb()?.all(`SELECT SUM(entry_price * size) as total_invested, COUNT(*) as open_count FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]) as any[] || [];\n const pp = paperPos[0] || { total_invested: 0, open_count: 0 };\n\n let liveInvested = 0, liveCurrentValue = 0, liveCount = 0, livePnl = 0;\n try {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n for (const p of posData) {\n if (parseFloat(p.size) <= 0) continue;\n liveInvested += parseFloat(p.initialValue) || 0;\n liveCurrentValue += parseFloat(p.currentValue) || 0;\n livePnl += parseFloat(p.cashPnl) || 0;\n liveCount++;\n }\n }\n } catch {}\n\n const pos = {\n total_invested: (pp.total_invested || 0) + liveInvested,\n current_value: liveCurrentValue,\n pnl: livePnl,\n open_count: (pp.open_count || 0) + liveCount,\n };\n\n const totalUsdc = usdceBalance + usdcNativeBalance;\n const needsSwap = usdceBalance < 1 && usdcNativeBalance > 1;\n\n return c.json({\n address,\n balances: {\n usdc: +totalUsdc.toFixed(6),\n usdce: +usdceBalance.toFixed(6),\n usdcNative: +usdcNativeBalance.toFixed(6),\n matic: +maticBalance.toFixed(6),\n exchange: +exchangeBalance.toFixed(6),\n },\n needsSwap,\n portfolio: {\n investedValue: +pos.total_invested.toFixed(2),\n currentValue: +pos.current_value.toFixed(2),\n pnl: +pos.pnl.toFixed(2),\n openPositions: pos.open_count || 0,\n totalValue: +pos.current_value.toFixed(2),\n },\n network: 'Polygon',\n depositAddress: address,\n depositInstructions: 'Send USDC.e (bridged USDC) on Polygon network to ' + address,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // On-chain transaction history — real USDC wallet transfers\n api.get('/polymarket/:agentId/wallet/transactions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const row = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n if (!row?.funder_address) return c.json({ error: 'No wallet configured' }, 404);\n const address = row.funder_address.toLowerCase();\n const page = parseInt(c.req.query('page') || '1');\n const pageSize = parseInt(c.req.query('pageSize') || '25');\n\n const RPC = 'https://polygon-bor-rpc.publicnode.com';\n const USDC_BRIDGED = USDC_E_SHARED;\n const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';\n const addrPadded = '0x000000000000000000000000' + address.slice(2);\n\n // Get current block\n const blockRes = await fetch(RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] })\n }).then(r => r.json()).catch(() => ({ result: '0x0' }));\n const currentBlock = parseInt(blockRes?.result || '0x0', 16);\n\n // Scan 50k-block chunks, up to 500k blocks back (~6 days on Polygon)\n const allLogs: any[] = [];\n const maxScan = 500000;\n const chunkSize = 49999;\n\n const fetches: Promise[] = [];\n for (let to = currentBlock; to > currentBlock - maxScan; to -= chunkSize) {\n const fromBlock = Math.max(currentBlock - maxScan, to - chunkSize);\n const toBlock = to;\n const fromHex = '0x' + fromBlock.toString(16);\n const toHex = '0x' + toBlock.toString(16);\n\n fetches.push(\n Promise.all([\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, null, addrPadded] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'in' }))); }).catch(() => {}),\n fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ fromBlock: fromHex, toBlock: toHex, address: USDC_BRIDGED, topics: [TRANSFER_TOPIC, addrPadded, null] }] })\n }).then(r => r.json()).then(d => { if (d?.result) allLogs.push(...d.result.map((l: any) => ({ ...l, direction: 'out' }))); }).catch(() => {}),\n ]).then(() => {})\n );\n }\n await Promise.all(fetches);\n\n // Parse logs into transactions\n const transactions = allLogs.map((log: any) => {\n const from = '0x' + (log.topics[1] || '').slice(26);\n const to = '0x' + (log.topics[2] || '').slice(26);\n const rawValue = BigInt(log.data || '0x0');\n const value = Number(rawValue) / 1e6;\n const blockNum = parseInt(log.blockNumber || '0x0', 16);\n return {\n hash: log.transactionHash,\n type: 'erc20',\n token: 'USDC.e',\n from, to,\n value: +value.toFixed(6),\n direction: log.direction,\n timestamp: blockNum, // Will use block as sort key\n block: blockNum,\n gas: null,\n status: 'confirmed',\n };\n });\n\n // Sort by block desc\n transactions.sort((a, b) => b.block - a.block);\n\n // Try to get timestamps from blocks (batch the unique blocks)\n const uniqueBlocks = [...new Set(transactions.map(t => t.block))];\n const blockTimestamps: Record = {};\n await Promise.all(uniqueBlocks.slice(0, 25).map(async (b) => {\n try {\n const res = await fetch(RPC, { method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x' + b.toString(16), false] })\n }).then(r => r.json());\n if (res?.result?.timestamp) blockTimestamps[b] = parseInt(res.result.timestamp, 16) * 1000;\n } catch {}\n }));\n for (const tx of transactions) {\n if (blockTimestamps[tx.block]) tx.timestamp = blockTimestamps[tx.block];\n }\n\n return c.json({ address, transactions, page, pageSize, total: transactions.length, hasMore: transactions.length >= pageSize });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Archive System ──────────────────────────────────────────\n\n // Create archive tables on first use (lazy init in the archive endpoint below)\n\n // Archive completed/closed data for a specific tab\n api.post('/polymarket/:agentId/archive', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { tab } = await c.req.json();\n const e = edb();\n const results: Record = {};\n\n // Lazy-create archive tables\n for (const t of ['poly_trade_log', 'poly_price_alerts', 'poly_watcher_events', 'poly_exit_rules', 'poly_watchers']) {\n await e?.run(`CREATE TABLE IF NOT EXISTS ${t}_archive (LIKE ${t} INCLUDING ALL)`).catch(() => {\n // Fallback for older Postgres or SQLite: create with same columns manually\n });\n }\n\n if (tab === 'trades' || tab === 'all') {\n // Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)\n await e?.run(`\n INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log \n WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n await e?.run(`\n DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')\n `, [agentId]).catch(() => {});\n\n // Also archive 'placed' trades for positions that no longer exist on-chain\n let closedCount = 0;\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = $1`, [agentId]) as any;\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}`);\n const posData = await posResp.json();\n const liveTokens = new Set(\n (Array.isArray(posData) ? posData : []).filter((p: any) => parseFloat(p.size) > 0).map((p: any) => p.asset)\n );\n // Get all placed trades\n const placedTrades = await e?.all(\n `SELECT id, token_id FROM poly_trade_log WHERE agent_id = $1 AND status = 'placed'`, [agentId]\n ) || [];\n const closedIds = (placedTrades as any[]).filter(t => !liveTokens.has(t.token_id)).map(t => t.id);\n if (closedIds.length > 0) {\n const placeholders = closedIds.map((_: any, i: number) => `$${i + 1}`).join(',');\n await e?.run(`INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => {});\n const del = await e?.run(`DELETE FROM poly_trade_log WHERE id IN (${placeholders})`, closedIds).catch(() => ({ changes: 0 }));\n closedCount = (del as any)?.rowCount || (del as any)?.changes || 0;\n }\n }\n } catch {}\n results.trades = closedCount;\n }\n\n if (tab === 'alerts' || tab === 'all') {\n // Archive triggered alerts\n const moved = await e?.run(`\n INSERT INTO poly_price_alerts_archive SELECT * FROM poly_price_alerts \n WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_price_alerts WHERE agent_id = $1 AND triggered = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.alerts = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'signals' || tab === 'all') {\n // Archive acknowledged watcher events\n const moved = await e?.run(`\n INSERT INTO poly_watcher_events_archive SELECT * FROM poly_watcher_events \n WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_watcher_events WHERE agent_id = $1 AND acknowledged = 1\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.signals = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'exits' || tab === 'all') {\n // Archive inactive exit rules\n const moved = await e?.run(`\n INSERT INTO poly_exit_rules_archive SELECT * FROM poly_exit_rules \n WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n const deleted = await e?.run(`\n DELETE FROM poly_exit_rules WHERE agent_id = $1 AND status IN ('fired', 'cancelled', 'expired')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.exits = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n if (tab === 'watchers' || tab === 'all') {\n // Archive paused/disabled watchers whose tokens no longer have positions\n // For 'all' tab: archive paused watchers. For 'watchers' tab: archive paused ones.\n await e?.run(`\n INSERT INTO poly_watchers_archive SELECT * FROM poly_watchers \n WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => {});\n const deleted = await e?.run(`\n DELETE FROM poly_watchers WHERE agent_id = $1 AND status IN ('paused', 'disabled')\n `, [agentId]).catch(() => ({ changes: 0 }));\n results.watchers = (deleted as any)?.rowCount || (deleted as any)?.changes || 0;\n }\n\n return c.json({ status: 'ok', archived: results });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // View archived data\n api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const tab = c.req.param('tab');\n const limit = parseInt(c.req.query('limit') || '50');\n const offset = parseInt(c.req.query('offset') || '0');\n const tableMap: Record = {\n trades: 'poly_trade_log_archive',\n alerts: 'poly_price_alerts_archive',\n signals: 'poly_watcher_events_archive',\n exits: 'poly_exit_rules_archive',\n watchers: 'poly_watchers_archive',\n };\n const table = tableMap[tab];\n if (!table) return c.json({ error: 'Invalid tab' }, 400);\n const rows = await edb()?.all(`SELECT * FROM ${table} WHERE agent_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`, [agentId, limit, offset]) || [];\n const countRow = await edb()?.get(`SELECT COUNT(*) as cnt FROM ${table} WHERE agent_id = $1`, [agentId]) as any;\n return c.json({ rows, total: countRow?.cnt || 0 });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ── Performance Goals System ──────────────────────────────\n // (poly_goals table is now created in ensurePolyDB above)\n\n // List goals\n api.get('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ goals: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Create goal\n api.post('/polymarket/:agentId/goals', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const id = 'goal_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);\n await edb()?.run(\n `INSERT INTO poly_goals (id, agent_id, name, type, period, target_value, notify_on_met, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [id, agentId, body.name, body.type, body.period || 'daily', body.target_value, body.notify_on_met !== false ? 1 : 0, 1]\n );\n return c.json({ id, status: 'created' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Update goal\n api.put('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n const body = await c.req.json();\n const sets: string[] = [];\n const vals: any[] = [];\n for (const k of ['name', 'type', 'period', 'target_value', 'notify_on_met', 'enabled']) {\n if (body[k] !== undefined) { sets.push(`${k} = ?`); vals.push(body[k]); }\n }\n if (sets.length === 0) return c.json({ error: 'Nothing to update' }, 400);\n sets.push(\"updated_at = CURRENT_TIMESTAMP\");\n vals.push(goalId);\n await edb()?.run(`UPDATE poly_goals SET ${sets.join(', ')} WHERE id = ?`, vals);\n return c.json({ status: 'updated' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete goal\n api.delete('/polymarket/:agentId/goals/:goalId', requireRole('admin'), async (c) => {\n try {\n const { goalId } = c.req.param() as any;\n await edb()?.run(`DELETE FROM poly_goals WHERE id = ?`, [goalId]);\n return c.json({ status: 'deleted' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Evaluate all goals for an agent (called by agent tool or on dashboard load)\n api.post('/polymarket/:agentId/goals/evaluate', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const goals = await edb()?.all(`SELECT * FROM poly_goals WHERE agent_id = ? AND enabled = 1`, [agentId]) as any[] || [];\n if (goals.length === 0) return c.json({ evaluated: 0, results: [] });\n\n // Gather current portfolio data\n const POLYGON_RPC = 'https://polygon.drpc.org';\n const USDC_CONTRACT = USDC_E_SHARED;\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch(POLYGON_RPC, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_CONTRACT, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Get paper trading stats\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n const weekStart = new Date(now.getTime() - 7 * 86400000).toISOString();\n const monthStart = new Date(now.getTime() - 30 * 86400000).toISOString();\n\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]) as any[] || [];\n const closedToday = paperPositions.filter((p: any) => p.closed && p.closed_at >= todayStart);\n const closedWeek = paperPositions.filter((p: any) => p.closed && p.closed_at >= weekStart);\n const closedMonth = paperPositions.filter((p: any) => p.closed && p.closed_at >= monthStart);\n const openPositions = paperPositions.filter((p: any) => !p.closed);\n\n const calcPnl = (positions: any[]) => positions.reduce((sum: number, p: any) => sum + (p.pnl || 0), 0);\n const calcWinRate = (positions: any[]) => {\n if (positions.length === 0) return 0;\n const wins = positions.filter((p: any) => (p.pnl || 0) > 0).length;\n return (wins / positions.length) * 100;\n };\n\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n const portfolioValue = usdcBalance + invested;\n\n // Count real trades from poly_trade_log (in addition to paper positions)\n const realTradesToday = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, todayStart]\n ).catch(() => ({ cnt: 0 })) as any;\n const realTradesWeek = await edb()?.get(\n `SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ? AND created_at::timestamptz > ?::timestamptz`,\n [agentId, weekStart]\n ).catch(() => ({ cnt: 0 })) as any;\n\n const paperTradesToday = closedToday.length + openPositions.filter((p: any) => p.created_at >= todayStart).length;\n const paperTradesWeek = closedWeek.length;\n\n // Metric lookup\n const metrics: Record = {\n daily_pnl_pct: invested > 0 ? (calcPnl(closedToday) / invested) * 100 : 0,\n daily_pnl_usd: calcPnl(closedToday),\n weekly_pnl_pct: invested > 0 ? (calcPnl(closedWeek) / invested) * 100 : 0,\n weekly_pnl_usd: calcPnl(closedWeek),\n monthly_pnl_pct: invested > 0 ? (calcPnl(closedMonth) / invested) * 100 : 0,\n monthly_pnl_usd: calcPnl(closedMonth),\n win_rate: calcWinRate([...closedToday, ...closedWeek]),\n total_trades_today: Math.max(paperTradesToday, parseInt(realTradesToday?.cnt || '0')),\n total_trades_week: Math.max(paperTradesWeek, parseInt(realTradesWeek?.cnt || '0')),\n portfolio_value: portfolioValue,\n balance: usdcBalance,\n open_positions: openPositions.length,\n max_drawdown: 0, // TODO: calculate from PnL timeline\n };\n\n const results: any[] = [];\n for (const goal of goals) {\n // Map goal type to metric\n let currentValue = 0;\n const typeMap: Record = {\n daily_pnl_pct: 'daily_pnl_pct', daily_pnl_usd: 'daily_pnl_usd',\n weekly_pnl_pct: 'weekly_pnl_pct', weekly_pnl_usd: 'weekly_pnl_usd',\n monthly_pnl_pct: 'monthly_pnl_pct', monthly_pnl_usd: 'monthly_pnl_usd',\n win_rate: 'win_rate', min_trades_daily: 'total_trades_today',\n min_trades_weekly: 'total_trades_week', portfolio_value: 'portfolio_value',\n max_drawdown: 'max_drawdown', balance_target: 'balance',\n };\n const metricKey = typeMap[goal.type] || goal.type;\n currentValue = metrics[metricKey] || 0;\n\n // For max_drawdown, goal is met when current is BELOW target (inverted)\n const isMaxType = goal.type === 'max_drawdown';\n const met = isMaxType ? (currentValue <= goal.target_value) : (currentValue >= goal.target_value);\n const wasMet = !!goal.met;\n const newStreak = met ? (goal.streak || 0) + 1 : 0;\n const bestStreak = Math.max(goal.best_streak || 0, newStreak);\n\n await edb()?.run(\n `UPDATE poly_goals SET current_value = ?, met = ?, streak = ?, best_streak = ?, times_met = times_met + ?, times_missed = times_missed + ?, last_evaluated = CAST(CURRENT_TIMESTAMP AS TEXT), met_at = CASE WHEN ? = 1 AND met = 0 THEN CAST(CURRENT_TIMESTAMP AS TEXT) ELSE met_at END, updated_at = CAST(CURRENT_TIMESTAMP AS TEXT) WHERE id = ?`,\n [currentValue, met ? 1 : 0, newStreak, bestStreak, (met && !wasMet) ? 1 : 0, (!met && wasMet) ? 1 : 0, met ? 1 : 0, goal.id]\n );\n\n const remaining = isMaxType ? 0 : Math.max(0, goal.target_value - currentValue);\n const progress = isMaxType ? (currentValue <= goal.target_value ? 100 : 0) : Math.min(100, goal.target_value > 0 ? (currentValue / goal.target_value) * 100 : 0);\n\n results.push({\n id: goal.id, name: goal.name, type: goal.type,\n target: goal.target_value, current: +currentValue.toFixed(4),\n met, justAchieved: met && !wasMet,\n remaining: +remaining.toFixed(4), progress: +progress.toFixed(1),\n streak: newStreak, bestStreak,\n });\n }\n\n return c.json({ evaluated: results.length, results, metrics });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // SSE price stream for watched markets\n api.get('/polymarket/:agentId/price-stream', requireRole('admin'), async (c) => {\n const agentId = c.req.param('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll Polymarket prices every 3 seconds for active positions\n const poll = setInterval(async () => {\n try {\n const e = edb();\n // Get active paper positions + live trades + alerts\n const paperRows = await e?.all(\n `SELECT DISTINCT token_id, market_question, side, entry_price, size FROM poly_paper_positions WHERE agent_id = ? AND closed = 0`, [agentId]\n ).catch(() => []) || [];\n // Live positions from Polymarket Data API (real on-chain)\n let liveTradeRows: any[] = [];\n const resolvedPrices: Record = {}; // token_id → resolved price (1.0 for winner, 0 for loser)\n try {\n const wallet = await e?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]);\n if (wallet?.funder_address) {\n const posResp = await fetch(`https://data-api.polymarket.com/positions?user=${wallet.funder_address}&sizeThreshold=0`);\n const posData = await posResp.json();\n if (Array.isArray(posData)) {\n liveTradeRows = posData.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => {\n // If position is redeemable or curPrice is exactly 0 or 1, it's resolved\n const curPrice = parseFloat(p.curPrice) || 0;\n if (p.redeemable || curPrice === 1 || curPrice === 0) {\n resolvedPrices[p.asset] = curPrice;\n }\n return {\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n redeemable: p.redeemable || false,\n resolved_price: p.redeemable ? curPrice : undefined,\n };\n });\n }\n }\n } catch {\n // Fallback to trade log netted\n const rawRows = await e?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL`, [agentId]\n ).catch(() => []) || [];\n const netMap = new Map();\n for (const r of rawRows as any[]) {\n if (!netMap.has(r.token_id)) netMap.set(r.token_id, { ...r, size: 0, side: 'BUY' });\n const entry = netMap.get(r.token_id)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n liveTradeRows = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n }\n const alertRows = await e?.all(\n `SELECT DISTINCT token_id, market_id FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]\n ).catch(() => []) || [];\n\n // Merge paper + live positions\n const allPositionRows = [...paperRows, ...liveTradeRows];\n\n if (allPositionRows.length === 0 && alertRows.length === 0) {\n send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));\n return;\n }\n\n // Fetch current prices for all unique token IDs\n const tokenIds = [...new Set([...allPositionRows.map((r: any) => r.token_id), ...alertRows.map((r: any) => r.token_id)].filter(Boolean))];\n const prices: Record = {};\n\n await Promise.all(tokenIds.map(async (tid: string) => {\n try {\n // Use resolved price from Data API if available (market ended)\n if (resolvedPrices[tid] !== undefined) {\n prices[tid] = resolvedPrices[tid];\n return;\n }\n const res = await fetch(`https://clob.polymarket.com/midpoint?token_id=${tid}`);\n const data = await res.json();\n prices[tid] = parseFloat(data?.mid || '0');\n } catch { /* skip */ }\n }));\n\n // Calculate P&L for each position (paper + live)\n const positions = allPositionRows.map((p: any) => {\n const currentPrice = prices[p.token_id] || p.entry_price;\n const pnl = p.side?.toLowerCase() === 'buy'\n ? (currentPrice - p.entry_price) * p.size\n : (p.entry_price - currentPrice) * p.size;\n // Use actual outcome from DB if available, otherwise derive from side\n const sideLower = (p.side || '').toLowerCase();\n const outcome = p.outcome\n ? p.outcome\n : (sideLower === 'buy' || sideLower === 'sell')\n ? (sideLower === 'buy' ? 'YES' : 'NO')\n : p.side;\n return {\n token_id: p.token_id,\n market: p.market_question,\n side: p.side,\n outcome: outcome,\n entry: p.entry_price,\n current: +currentPrice.toFixed(4),\n size: p.size,\n pnl: +pnl.toFixed(2),\n pnlPct: +((pnl / (p.entry_price * p.size)) * 100).toFixed(2),\n redeemable: p.redeemable || false,\n resolved: resolvedPrices[p.token_id] !== undefined,\n };\n });\n\n const totalPnl = positions.reduce((s: number, p: any) => s + p.pnl, 0);\n\n send(JSON.stringify({\n type: 'prices',\n positions,\n totalPnl: +totalPnl.toFixed(2),\n ts: Date.now(),\n }));\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 3000);\n\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // Get alerts for an agent\n api.get('/polymarket/:agentId/alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_price_alerts WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ alerts: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete an alert\n api.delete('/polymarket/alerts/:alertId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_price_alerts WHERE id = ?`, [c.req.param('alertId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get paper positions for an agent\n api.get('/polymarket/:agentId/paper', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ? ORDER BY created_at DESC`, [agentId]) || [];\n return c.json({ positions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Live positions (real trades with CLOB order IDs)\n api.get('/polymarket/:agentId/live-positions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n // Get wallet address for this agent\n const wallet = await edb()?.get(\n `SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]\n );\n const address = wallet?.funder_address;\n if (!address) return c.json({ positions: [] });\n\n // Fetch real positions from Polymarket Data API\n try {\n const resp = await fetch(`https://data-api.polymarket.com/positions?user=${address}`);\n const data = await resp.json();\n if (Array.isArray(data)) {\n const positions = data.filter((p: any) => parseFloat(p.size) > 0).map((p: any) => ({\n id: p.conditionId || p.asset,\n token_id: p.asset,\n market_question: p.title || 'Unknown',\n outcome: p.outcome || '',\n side: 'BUY',\n entry_price: parseFloat(p.avgPrice) || 0,\n size: parseFloat(p.size) || 0,\n current_price: parseFloat(p.curPrice) || 0,\n pnl: parseFloat(p.cashPnl) || 0,\n pnl_pct: parseFloat(p.percentPnl) || 0,\n initial_value: parseFloat(p.initialValue) || 0,\n current_value: parseFloat(p.currentValue) || 0,\n position_type: 'live',\n created_at: null,\n }));\n return c.json({ positions });\n }\n } catch (apiErr: any) {\n console.warn(`[live-positions] Data API failed, falling back to trade log: ${apiErr.message}`);\n }\n\n // Fallback: trade log (but net out buys/sells per token)\n const rows = await edb()?.all(\n `SELECT token_id, market_question, outcome, side, price as entry_price, size, created_at\n FROM poly_trade_log WHERE agent_id = ? AND status = 'placed' AND clob_order_id IS NOT NULL\n ORDER BY created_at DESC`, [agentId]\n ) || [];\n // Net positions by token_id\n const netMap = new Map();\n for (const r of rows as any[]) {\n const key = r.token_id;\n if (!netMap.has(key)) netMap.set(key, { ...r, size: 0, side: 'BUY', position_type: 'live' });\n const entry = netMap.get(key)!;\n entry.size += r.side === 'BUY' ? parseFloat(r.size) : -parseFloat(r.size);\n }\n const positions = Array.from(netMap.values()).filter((p: any) => p.size > 0.01);\n return c.json({ positions });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Manual trade execution from dashboard\n api.post('/polymarket/:agentId/manual-trade', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const { token_id, side, size, market_question, outcome } = body;\n if (!token_id || !side || !size) return c.json({ error: 'Missing token_id, side, or size' }, 400);\n if (!['BUY', 'SELL'].includes(side)) return c.json({ error: 'side must be BUY or SELL' }, 400);\n if (size < 1) return c.json({ error: 'Minimum size is 1 share' }, 400);\n\n // For BUY orders, validate against wallet balance\n if (side === 'BUY') {\n try {\n const walletRow = await edb()?.get(`SELECT address FROM poly_wallets WHERE agent_id = ?`, [agentId]) as any;\n if (walletRow?.address) {\n const USDC_E = USDC_E_SHARED;\n const balResp = await fetch(`https://polygon-bor-rpc.publicnode.com`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_E, data: '0x70a08231000000000000000000000000' + walletRow.address.slice(2) }, 'latest'] }),\n signal: AbortSignal.timeout(6000),\n });\n const balData = await balResp.json();\n const usdcBal = parseInt(balData?.result || '0', 16) / 1e6;\n // Quick cost estimate: price * size\n const midResp2 = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(6000) });\n const midData2 = await midResp2.json();\n const estPrice = parseFloat(midData2?.mid || '0');\n if (estPrice > 0 && estPrice * size > usdcBal) {\n return c.json({ error: `Insufficient funds: need $${(estPrice * size).toFixed(2)} but wallet has $${usdcBal.toFixed(2)} USDC` }, 400);\n }\n }\n } catch {} // Don't block trade if balance check fails\n }\n\n // Get current midpoint price\n const midResp = await fetch(`https://clob.polymarket.com/midpoint?token_id=${token_id}`, { signal: AbortSignal.timeout(8000) });\n const midData = await midResp.json();\n const price = parseFloat(midData?.mid || '0');\n if (!price) return c.json({ error: 'Could not fetch current price' }, 400);\n\n const tradeId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const e = edb();\n // The polymarket tools use $N-style params which can be REUSED (e.g. ON CONFLICT SET col=$2).\n // edb() converts ?→$N internally, which breaks reused params (15 ?'s but 8 values).\n // Use rawQuery/rawExec which pass $N params directly to Postgres without conversion.\n const dbIface = {\n execute: async (sql: string, params?: any[]) => (e as any)?.rawExec?.(sql, params) || e?.run(sql.replace(/\\$(\\d+)/g, '?'), params),\n query: async (sql: string, params?: any[]) => (e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params),\n get: async (sql: string, params?: any[]) => { const rows = await ((e as any)?.rawQuery?.(sql, params) || e?.all(sql.replace(/\\$(\\d+)/g, '?'), params)); return rows?.[0]; },\n getEngineDB: () => e,\n };\n\n // Verify wallet is accessible before attempting trade\n const testCreds = await e?.all('SELECT agent_id FROM poly_wallet_credentials WHERE agent_id = ?', [agentId]);\n if (!testCreds || testCreds.length === 0) {\n return c.json({ error: 'No wallet credentials found for this agent. Set up a wallet first.' }, 400);\n }\n\n console.log(`[manual-trade] ${side} ${size} shares, agent=${agentId}, token=${token_id.slice(0,12)}..., price=${price}`);\n\n // Clear cached CLOB client (may have failed previously with bad db wrapper)\n try {\n const { getClobClient } = await import('../agent-tools/tools/polymarket-runtime.js');\n // clientInstances is module-level cache — if it previously failed, it won't have an entry\n // so getClobClient will re-derive. Just verify it works now:\n const testClient = await (getClobClient as any)(agentId, dbIface);\n if (!testClient) {\n return c.json({ error: 'Could not initialize trading client. Check wallet setup and SDK.' }, 500);\n }\n console.log(`[manual-trade] CLOB client ready for ${agentId}`);\n } catch (preErr: any) {\n console.log(`[manual-trade] CLOB client error:`, preErr.message);\n return c.json({ error: 'Trading client error: ' + preErr.message }, 500);\n }\n\n const result = await executeOrder(agentId, dbIface, tradeId, {\n token_id, side, price, size,\n order_type: 'GTC',\n market_question: market_question || 'Manual trade',\n outcome: outcome || '',\n rationale: 'Manual trade from dashboard',\n }, 'manual_dashboard');\n\n const parsed = typeof result === 'string' ? JSON.parse(result) : result;\n console.log(`[manual-trade] Result status: ${parsed?.status || parsed?.error || 'unknown'}`, JSON.stringify(parsed).slice(0, 200));\n\n // If executeOrder returned an error status, surface it properly\n if (parsed?.status === 'no_wallet' || parsed?.status === 'error' || parsed?.error) {\n return c.json({ error: parsed?.message || parsed?.error || 'Trade failed: ' + (parsed?.status || 'unknown') }, 400);\n }\n return c.json({ status: 'ok', trade: parsed });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Search markets for manual trading — full screener analysis + orderbook\n api.get('/polymarket/markets/search', requireRole('admin'), async (c) => {\n try {\n const q = c.req.query('q') || '';\n const strategy = c.req.query('strategy') || 'best_opportunities';\n if (!q || q.length < 2) return c.json({ markets: [] });\n\n // Fetch from screener + direct Gamma search for maximum coverage\n const { screenMarkets } = await import('../polymarket-engines/screener.js');\n const { apiFetch, GAMMA_API } = await import('../polymarket-engines/shared.js');\n\n // Run screener and direct Gamma search in parallel\n const [result, directMarkets, directEvents] = await Promise.all([\n (screenMarkets as any)({ query: q, strategy, limit: 20, includeOrderbook: true }).catch(() => ({ markets: [], scanned: 0, qualified: 0 })),\n apiFetch(`${GAMMA_API}/markets?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n apiFetch(`${GAMMA_API}/events?search=${encodeURIComponent(q)}&active=true&closed=false&limit=50&order=volume&ascending=false`).catch(() => []),\n ]);\n\n // Extract markets from events\n const eventMarkets: any[] = [];\n if (Array.isArray(directEvents)) {\n for (const ev of directEvents) {\n if (ev.markets && Array.isArray(ev.markets)) {\n for (const m of ev.markets) {\n if (m.active && !m.closed) eventMarkets.push(m);\n }\n }\n }\n }\n\n // Merge direct results as extra scored markets (with basic scoring)\n const screenerIds = new Set((result.markets || []).map((sm: any) => sm.market?.id || sm.market?.conditionId));\n const allDirect = [...(Array.isArray(directMarkets) ? directMarkets : []), ...eventMarkets];\n const { parseMarket } = await import('../polymarket-engines/shared.js');\n for (const raw of allDirect) {\n const pm = parseMarket(raw);\n if (!pm.active || pm.clobTokenIds.length === 0 || pm.liquidity < 1) continue;\n const key = pm.id;\n if (screenerIds.has(key)) continue;\n screenerIds.add(key);\n // Add with basic scores\n const yesP = pm.outcomePrices[0] || 0.5;\n result.markets.push({\n market: pm,\n scores: { total: Math.min(pm.liquidity / 100, 20) + Math.min(pm.volume / 10000, 20), liquidity: Math.min(pm.liquidity / 100, 20), volume: Math.min(pm.volume / 10000, 20), spread: 0, edge: 0, timing: 0, momentum: 0 },\n analysis: { overround: (yesP + (pm.outcomePrices[1] || 0.5) - 1) * 100, orderbook: null },\n recommendation: { action: 'watch', confidence: 0 },\n });\n }\n // Re-sort by score\n result.markets.sort((a: any, b: any) => (b.scores?.total || 0) - (a.scores?.total || 0));\n\n // Filter out garbage markets (low scores, extreme spreads, dead liquidity)\n const filtered = (result.markets || []).filter((sm: any) => {\n const spreadPct = sm.analysis?.orderbook?.spreadPct ?? 0;\n const liq = sm.market?.liquidity || 0;\n const prices = sm.market?.outcomePrices || [];\n // Kill extreme-price markets (Yes ≤1¢ or ≥99.5¢ = truly decided)\n const yesP = prices[0] || 0.5;\n if (yesP <= 0.01 || yesP >= 0.995) return false;\n // Kill markets with insane spread (>500%)\n if (spreadPct > 500) return false;\n // Kill truly dead markets (<$50 liquidity)\n if (liq < 50) return false;\n return true;\n });\n\n // Run unified pipeline on top results (quick depth: quant + onchain)\n const { quickAnalysis } = await import('../polymarket-engines/pipeline.js');\n const top10 = filtered.slice(0, 10).filter((sm: any) => sm.market?.clobTokenIds?.[0]);\n\n let pipelineResults: any[] = [];\n try {\n pipelineResults = await Promise.all(top10.map((sm: any) => {\n const m = sm.market || {};\n return quickAnalysis(m.clobTokenIds[0], m.question, 100).then(r => ({ ...r, tokenId: m.clobTokenIds[0] })).catch(() => null);\n }));\n pipelineResults = pipelineResults.filter(Boolean);\n } catch (pipeErr: any) {\n console.log('[pipeline] Error:', pipeErr.message);\n }\n\n // Index pipeline results by tokenId for lookup\n const pipelineMap = new Map();\n for (const pr of pipelineResults) pipelineMap.set(pr.tokenId, pr);\n\n // Transform screener output to frontend format\n const markets = filtered.map((sm: any) => {\n const m = sm.market || {};\n const tokens = m.clobTokenIds || [];\n const outcomes = m.outcomes || ['Yes', 'No'];\n const prices: Record = {};\n const spread: Record = {};\n for (let i = 0; i < outcomes.length; i++) {\n prices[outcomes[i]] = m.outcomePrices?.[i] || 0;\n const ob = sm.analysis?.orderbook;\n if (ob && i === 0) {\n spread[outcomes[i]] = { bid: ob.bestBid || 0, ask: ob.bestAsk || 0, depth: Math.min(ob.bidDepth5 || 0, ob.askDepth5 || 0) };\n // Compute complementary spread for second outcome\n if (outcomes[1]) spread[outcomes[1]] = { bid: 1 - (ob.bestAsk || 1), ask: 1 - (ob.bestBid || 0), depth: spread[outcomes[0]].depth };\n }\n }\n return {\n id: m.id, question: m.question, slug: m.slug,\n tokens, outcomes, prices, spread,\n volume24hr: m.volume24hr || m.volume || 0,\n liquidity: m.liquidity || 0,\n endDate: m.endDate, startDate: m.startDate,\n // Screener scores\n scores: sm.scores || {},\n analysis: {\n // Quantitative analysis\n overround: sm.analysis?.overround,\n hoursToClose: sm.analysis?.hoursToClose,\n volumePerHour: sm.analysis?.volumePerHour,\n priceLevel: sm.analysis?.priceLevel,\n edgeType: sm.analysis?.edgeType,\n orderbook: sm.analysis?.orderbook ? {\n bestBid: sm.analysis.orderbook.bestBid,\n bestAsk: sm.analysis.orderbook.bestAsk,\n spreadPct: sm.analysis.orderbook.spreadPct,\n bidDepth5: sm.analysis.orderbook.bidDepth5,\n askDepth5: sm.analysis.orderbook.askDepth5,\n imbalance: sm.analysis.orderbook.imbalance,\n } : null,\n },\n // Screener recommendation\n recommendation: sm.recommendation || null,\n // Pipeline enrichment (quant + onchain + composite)\n pipeline: (() => {\n const pr = pipelineMap.get(m.clobTokenIds?.[0]);\n if (!pr) return null;\n return {\n score: pr.score,\n action: pr.action,\n thesis: pr.thesis,\n kelly: pr.kelly,\n regime: pr.regime,\n smart_money: pr.smart_money,\n manipulation_risk: pr.manipulation_risk,\n orderbook: pr.orderbook,\n };\n })(),\n };\n });\n\n return c.json({ markets, strategy: result.strategy, scanned: result.scanned, qualified: result.qualified, summary: result.summary });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Pause/resume trading\n api.post('/polymarket/:agentId/pause', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const { action, reason } = await c.req.json();\n const today = new Date().toISOString().split('T')[0];\n if (action === 'pause') {\n await edb()?.run(`\n INSERT INTO poly_daily_counters (agent_id, date, paused, pause_reason) VALUES (?, ?, 1, ?)\n ON CONFLICT (agent_id, date) DO UPDATE SET paused = 1, pause_reason = ?\n `, [agentId, today, reason || 'Admin paused', reason || 'Admin paused']);\n } else {\n await edb()?.run(`UPDATE poly_daily_counters SET paused = 0, pause_reason = '' WHERE agent_id = ? AND date = ?`, [agentId, today]);\n }\n return c.json({ status: 'ok', action });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Dashboard summary — all polymarket agents\n api.get('/polymarket/dashboard', requireRole('admin'), async (c) => {\n try {\n const configs = await edb()?.all(`SELECT * FROM poly_trading_config`).catch(() => []) || [];\n const wallets = await edb()?.all(`SELECT agent_id, funder_address, signature_type FROM poly_wallet_credentials`).catch(() => []) || [];\n const pendingTrades = await edb()?.all(`SELECT agent_id, COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending' GROUP BY agent_id`).catch(() => []) || [];\n const today = new Date().toISOString().split('T')[0];\n const dailyCounters = await edb()?.all(`SELECT * FROM poly_daily_counters WHERE date = ?`, [today]).catch(() => []) || [];\n return c.json({ configs, wallets, pendingTrades, dailyCounters });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Daily scorecard for a specific agent\n api.get('/polymarket/:agentId/daily-scorecard', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const today = new Date().toISOString().split('T')[0];\n const todayStart = new Date(new Date().setHours(0, 0, 0, 0)).toISOString();\n\n // Get wallet balance\n const walletRow = await edb()?.get(`SELECT funder_address FROM poly_wallet_credentials WHERE agent_id = ?`, [agentId]) as any;\n let usdcBalance = 0;\n if (walletRow?.funder_address) {\n try {\n const balRes = await fetch('https://polygon.drpc.org', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [\n { to: USDC_E_SHARED, data: '0x70a08231000000000000000000000000' + walletRow.funder_address.slice(2).toLowerCase() }, 'latest'\n ] })\n }).then(r => r.json());\n usdcBalance = Number(BigInt(balRes?.result || '0x0')) / 1e6;\n } catch {}\n }\n\n // Daily counters\n const counter = await edb()?.get(`SELECT * FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => null) as any;\n\n // Paper positions (daily P&L)\n const paperPositions = await edb()?.all(`SELECT * FROM poly_paper_positions WHERE agent_id = ?`, [agentId]).catch(() => []) as any[];\n const closedToday = (paperPositions || []).filter((p: any) => p.closed && p.closed_at >= todayStart);\n const openPositions = (paperPositions || []).filter((p: any) => !p.closed);\n const dailyPnl = closedToday.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const unrealizedPnl = openPositions.reduce((s: number, p: any) => s + (p.pnl || 0), 0);\n const winsToday = closedToday.filter((p: any) => (p.pnl || 0) > 0).length;\n const lossesToday = closedToday.filter((p: any) => (p.pnl || 0) < 0).length;\n\n // Daily goal target\n const dailyGoal = await edb()?.get(`SELECT target_value FROM poly_goals WHERE agent_id = ? AND type IN ('daily_pnl_usd', 'daily_pnl_pct') AND enabled = 1 ORDER BY type ASC LIMIT 1`, [agentId]).catch(() => null) as any;\n const dailyTarget = dailyGoal?.target_value || 0;\n\n const totalPnl = dailyPnl + unrealizedPnl;\n const progressPct = dailyTarget > 0 ? (totalPnl / dailyTarget) * 100 : 0;\n const tradesToday = counter?.trade_count || closedToday.length;\n const winRate = tradesToday > 0 ? (winsToday / tradesToday) * 100 : 0;\n const invested = openPositions.reduce((s: number, p: any) => s + ((p.entry_price || 0) * (p.size || 0)), 0);\n\n let status = 'ON_TRACK';\n if (totalPnl <= -(counter?.max_daily_loss || 50)) status = 'STOP_TRADING';\n else if (progressPct >= 100) status = 'TARGET_HIT';\n else if (progressPct >= 70) status = 'AHEAD';\n else if (progressPct < 30 && tradesToday > 3) status = 'BEHIND';\n\n return c.json({\n date: today,\n total_pnl: +totalPnl.toFixed(2),\n realized_pnl: +dailyPnl.toFixed(2),\n unrealized_pnl: +unrealizedPnl.toFixed(2),\n daily_target: dailyTarget,\n target_progress_pct: +progressPct.toFixed(1),\n trades_today: tradesToday,\n wins_today: winsToday,\n losses_today: lossesToday,\n win_rate_today: +winRate.toFixed(1),\n open_positions: openPositions.length,\n available_capital: +usdcBalance.toFixed(2),\n deployed_capital: +invested.toFixed(2),\n daily_loss: +(counter?.daily_loss || 0).toFixed(2),\n paused: !!counter?.paused,\n status,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ═══ POLYMARKET REAL-TIME STREAM ══════════════════════════════\n\n // SSE stream for polymarket dashboard updates\n api.get('/polymarket/stream', requireRole('admin'), async (c) => {\n const agentId = c.req.query('agentId');\n const stream = new ReadableStream({\n start(controller) {\n const encoder = new TextEncoder();\n const send = (data: string) => {\n try { controller.enqueue(encoder.encode(`data: ${data}\\n\\n`)); }\n catch { clearInterval(poll); }\n };\n\n // Send initial heartbeat\n send(JSON.stringify({ type: 'connected' }));\n\n // Poll for changes every 5 seconds (lightweight — just counts + status)\n let lastPendingCount = -1;\n let lastTradeCount = -1;\n let lastAlertCount = -1;\n let lastPredCount = -1;\n let lastPaused = false;\n\n const poll = setInterval(async () => {\n try {\n // Lightweight queries — just counts\n const e = edb();\n const pendingRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE agent_id = ? AND status = 'pending'`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_pending_trades WHERE status = 'pending'`).catch(() => ({ cnt: 0 }));\n const pendingCount = (pendingRow as any)?.cnt ?? 0;\n\n const tradeRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_trade_log`).catch(() => ({ cnt: 0 }));\n const tradeCount = (tradeRow as any)?.cnt ?? 0;\n\n const alertRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE agent_id = ? AND triggered = 0`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_price_alerts WHERE triggered = 0`).catch(() => ({ cnt: 0 }));\n const alertCount = (alertRow as any)?.cnt ?? 0;\n\n const predRow = agentId\n ? await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions WHERE agent_id = ?`, [agentId]).catch(() => ({ cnt: 0 }))\n : await e?.get(`SELECT COUNT(*) as cnt FROM poly_predictions`).catch(() => ({ cnt: 0 }));\n const predCount = (predRow as any)?.cnt ?? 0;\n\n const today = new Date().toISOString().split('T')[0];\n const pauseRow = agentId\n ? await e?.get(`SELECT paused FROM poly_daily_counters WHERE agent_id = ? AND date = ?`, [agentId, today]).catch(() => undefined)\n : await e?.get(`SELECT paused FROM poly_daily_counters WHERE date = ? LIMIT 1`, [today]).catch(() => undefined);\n const isPaused = !!(pauseRow as any)?.paused;\n\n // Only send if something changed\n const changed = pendingCount !== lastPendingCount || tradeCount !== lastTradeCount ||\n alertCount !== lastAlertCount || predCount !== lastPredCount || isPaused !== lastPaused;\n\n if (changed) {\n lastPendingCount = pendingCount;\n lastTradeCount = tradeCount;\n lastAlertCount = alertCount;\n lastPredCount = predCount;\n lastPaused = isPaused;\n\n send(JSON.stringify({\n type: 'update',\n pending: pendingCount,\n trades: tradeCount,\n alerts: alertCount,\n predictions: predCount,\n paused: isPaused,\n timestamp: new Date().toISOString(),\n }));\n } else {\n // Keepalive\n send(JSON.stringify({ type: 'heartbeat' }));\n }\n } catch (err: any) {\n send(JSON.stringify({ type: 'error', message: err.message }));\n }\n }, 5000);\n\n // Cleanup\n c.req.raw.signal?.addEventListener('abort', () => { clearInterval(poll); });\n },\n });\n\n return new Response(stream, {\n headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },\n });\n });\n\n // ═══ POLYMARKET LEARNING / JOURNAL ════════════════════════════\n\n // Get predictions for an agent\n api.get('/polymarket/:agentId/predictions', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const resolved = c.req.query('resolved');\n const limit = parseInt(c.req.query('limit') || '50');\n const query = resolved !== undefined\n ? `SELECT * FROM poly_predictions WHERE agent_id = ? AND resolved = ? ORDER BY created_at DESC LIMIT ?`\n : `SELECT * FROM poly_predictions WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`;\n const params = resolved !== undefined ? [agentId, resolved === 'true' ? 1 : 0, limit] : [agentId, limit];\n const rows = await edb()?.all(query, params).catch(() => []) || [];\n return c.json({ predictions: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get calibration data\n api.get('/polymarket/:agentId/calibration', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_calibration WHERE agent_id = ? ORDER BY bucket`, [agentId]).catch(() => []) || [];\n return c.json({ calibration: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get strategy performance\n api.get('/polymarket/:agentId/strategies', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`\n SELECT *, CASE WHEN total_predictions > 0 THEN ROUND(CAST(correct_predictions AS REAL) / total_predictions * 100, 1) ELSE 0 END as win_rate\n FROM poly_strategy_stats WHERE agent_id = ? ORDER BY total_pnl DESC\n `, [agentId]).catch(() => []) || [];\n return c.json({ strategies: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Get lessons\n api.get('/polymarket/:agentId/lessons', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(`SELECT * FROM poly_lessons WHERE agent_id = ? ORDER BY importance DESC, created_at DESC`, [agentId]).catch(() => []) || [];\n return c.json({ lessons: rows });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Delete a lesson\n api.delete('/polymarket/lessons/:lessonId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(`DELETE FROM poly_lessons WHERE id = ?`, [c.req.param('lessonId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: On-Chain Intelligence ──────────────────────\n api.get('/polymarket/:agentId/whales', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_whale_wallets ORDER BY total_volume DESC LIMIT 50').catch(() => []) || [];\n return c.json({ whales: rows });\n } catch { return c.json({ whales: [] }); }\n });\n\n api.get('/polymarket/:agentId/flow', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_flow_snapshots ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Social Intelligence ──────────────────────\n api.get('/polymarket/:agentId/social', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_social_signals ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ signals: rows });\n } catch { return c.json({ signals: [] }); }\n });\n\n api.get('/polymarket/:agentId/social/watchlist', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_social_watchlist WHERE agent_id = ? AND active = 1', [agentId]).catch(() => []) || [];\n return c.json({ watchlist: rows });\n } catch { return c.json({ watchlist: [] }); }\n });\n\n // ─── Polymarket: Event Calendar ──────────────────────\n api.get('/polymarket/:agentId/events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_event_calendar WHERE agent_id = ? ORDER BY event_date ASC', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, related_markets: JSON.parse(r.related_markets || '[]') })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.delete('/polymarket/events/:eventId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run('DELETE FROM poly_event_calendar WHERE id = ?', [c.req.param('eventId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/news-alerts', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_news_alerts WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ alerts: rows });\n } catch { return c.json({ alerts: [] }); }\n });\n\n // ─── Polymarket: Analytics ──────────────────────\n api.get('/polymarket/:agentId/correlations', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_correlations WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 50', [agentId]).catch(() => []) || [];\n return c.json({ correlations: rows });\n } catch { return c.json({ correlations: [] }); }\n });\n\n api.get('/polymarket/:agentId/arbitrage', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_arb_opportunities WHERE agent_id = ? AND status = 'open' ORDER BY timestamp DESC LIMIT 30\", [agentId]).catch(() => []) || [];\n return c.json({ opportunities: rows.map((r: any) => ({ ...r, markets: JSON.parse(r.markets || '{}') })) });\n } catch { return c.json({ opportunities: [] }); }\n });\n\n api.get('/polymarket/:agentId/regimes', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_regime_signals ORDER BY timestamp DESC LIMIT 50').catch(() => []) || [];\n return c.json({ regimes: rows });\n } catch { return c.json({ regimes: [] }); }\n });\n\n // ─── Polymarket: Execution ──────────────────────\n api.get('/polymarket/:agentId/snipers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_sniper_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ snipers: rows });\n } catch { return c.json({ snipers: [] }); }\n });\n\n api.delete('/polymarket/snipers/:sniperId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_sniper_orders SET status = 'cancelled' WHERE id = ?\", [c.req.param('sniperId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.get('/polymarket/:agentId/scale-orders', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_scale_orders WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ orders: rows });\n } catch { return c.json({ orders: [] }); }\n });\n\n api.get('/polymarket/:agentId/hedges', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_hedges WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ hedges: rows });\n } catch { return c.json({ hedges: [] }); }\n });\n\n api.get('/polymarket/:agentId/exit-rules', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all(\"SELECT * FROM poly_exit_rules WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC\", [agentId]).catch(() => []) || [];\n return c.json({ rules: rows });\n } catch { return c.json({ rules: [] }); }\n });\n\n api.delete('/polymarket/exit-rules/:ruleId', requireRole('admin'), async (c) => {\n try {\n await edb()?.run(\"UPDATE poly_exit_rules SET status = 'removed' WHERE id = ?\", [c.req.param('ruleId')]);\n return c.json({ status: 'ok' });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Portfolio ──────────────────────\n api.get('/polymarket/:agentId/drawdown', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_portfolio_snapshots WHERE agent_id = ? ORDER BY timestamp DESC LIMIT 100', [agentId]).catch(() => []) || [];\n const peak = rows.length ? Math.max(...rows.map((r: any) => r.total_value)) : 0;\n const current = rows[0]?.total_value || 0;\n return c.json({ snapshots: rows, peak, current, drawdown_pct: peak > 0 ? +((peak - current) / peak * 100).toFixed(2) : 0 });\n } catch { return c.json({ snapshots: [], peak: 0, current: 0, drawdown_pct: 0 }); }\n });\n\n api.get('/polymarket/:agentId/pnl-attribution', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const e = edb();\n const byStrategy = await e?.all(`SELECT strategy, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl, AVG(avg_hold_hours) as avg_hold\n FROM poly_pnl_attribution WHERE agent_id = ? AND strategy IS NOT NULL GROUP BY strategy ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const byCategory = await e?.all(`SELECT category, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND category IS NOT NULL GROUP BY category ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n const bySignal = await e?.all(`SELECT signal_source, COUNT(*) as trades, SUM(wins) as wins, SUM(net_pnl) as total_pnl\n FROM poly_pnl_attribution WHERE agent_id = ? AND signal_source IS NOT NULL GROUP BY signal_source ORDER BY total_pnl DESC`, [agentId]).catch(() => []) || [];\n return c.json({ byStrategy, byCategory, bySignal });\n } catch { return c.json({ byStrategy: [], byCategory: [], bySignal: [] }); }\n });\n\n // ─── Polymarket: Counter-Intelligence ──────────────────────\n api.get('/polymarket/:agentId/odds-snapshots', requireRole('admin'), async (c) => {\n try {\n const rows = await edb()?.all('SELECT * FROM poly_odds_snapshots ORDER BY timestamp DESC LIMIT 100').catch(() => []) || [];\n return c.json({ snapshots: rows });\n } catch { return c.json({ snapshots: [] }); }\n });\n\n // ─── Polymarket: Watchers / Automation ────────────────────\n api.get('/polymarket/:agentId/watchers', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watchers WHERE agent_id = ? ORDER BY created_at DESC', [agentId]).catch(() => []) || [];\n return c.json({ watchers: rows.map((r: any) => ({ ...r, config: (() => { try { return JSON.parse(r.config || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ watchers: [] }); }\n });\n\n api.get('/polymarket/:agentId/watcher-events', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const rows = await edb()?.all('SELECT * FROM poly_watcher_events WHERE agent_id = ? ORDER BY created_at DESC LIMIT 200', [agentId]).catch(() => []) || [];\n return c.json({ events: rows.map((r: any) => ({ ...r, data: (() => { try { return JSON.parse(r.data || '{}'); } catch { return {}; } })() })) });\n } catch { return c.json({ events: [] }); }\n });\n\n api.post('/polymarket/watchers/:id/toggle', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n const w = await edb()?.get('SELECT status FROM poly_watchers WHERE id = ?', [id]);\n const newStatus = w?.status === 'active' ? 'paused' : 'active';\n await edb()?.run('UPDATE poly_watchers SET status = ? WHERE id = ?', [newStatus, id]);\n return c.json({ success: true, status: newStatus });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.delete('/polymarket/watchers/:id', requireRole('admin'), async (c) => {\n try {\n const id = c.req.param('id');\n await edb()?.run('DELETE FROM poly_watchers WHERE id = ?', [id]);\n await edb()?.run('DELETE FROM poly_watcher_events WHERE watcher_id = ?', [id]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/:agentId/watcher-events/acknowledge-all', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n await edb()?.run('UPDATE poly_watcher_events SET acknowledged = 1 WHERE agent_id = ? AND acknowledged = 0', [agentId]);\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher AI Config ─────────────────────────\n api.get('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const cfg = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (!cfg) return c.json({ configured: false });\n return c.json({\n configured: true,\n provider: cfg.ai_provider,\n model: cfg.ai_model,\n has_api_key: !!cfg.ai_api_key,\n has_custom_key: !!(cfg.ai_api_key && !cfg.use_org_key),\n use_org_key: cfg.use_org_key !== 0,\n budget_daily: cfg.analysis_budget_daily,\n used_today: cfg.analysis_count_today,\n remaining_today: (cfg.analysis_budget_daily || 100) - (cfg.analysis_count_today || 0),\n max_spawn_per_hour: cfg.max_spawn_per_hour,\n });\n } catch { return c.json({ configured: false }); }\n });\n\n api.post('/polymarket/:agentId/watcher-config', requireRole('admin'), async (c) => {\n try {\n const agentId = c.req.param('agentId');\n const body = await c.req.json();\n const existing = await edb()?.get('SELECT * FROM poly_watcher_config WHERE agent_id = ?', [agentId]).catch(() => null);\n if (existing) {\n const updates: string[] = []; const vals: any[] = [];\n if (body.ai_provider) { updates.push('ai_provider = ?'); vals.push(body.ai_provider); }\n if (body.ai_model) { updates.push('ai_model = ?'); vals.push(body.ai_model); }\n if (body.ai_api_key) { updates.push('ai_api_key = ?'); vals.push(body.ai_api_key); }\n if (body.use_org_key != null) { updates.push('use_org_key = ?'); vals.push(body.use_org_key ? 1 : 0); }\n // When using org key, resolve and store it\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) {\n const resolved = vault.decrypt(encKey);\n updates.push('ai_api_key = ?'); vals.push(resolved);\n }\n } else if (!body.use_org_key && !body.ai_api_key) { /* keep existing custom key */ }\n if (body.analysis_budget_daily != null) { updates.push('analysis_budget_daily = ?'); vals.push(body.analysis_budget_daily); }\n if (body.max_spawn_per_hour != null) { updates.push('max_spawn_per_hour = ?'); vals.push(body.max_spawn_per_hour); }\n updates.push('updated_at = CURRENT_TIMESTAMP');\n vals.push(agentId);\n await edb()?.run(`UPDATE poly_watcher_config SET ${updates.join(', ')} WHERE agent_id = ?`, vals);\n } else {\n // Resolve API key\n let resolvedKey = body.ai_api_key || '';\n if (body.use_org_key && body.ai_provider) {\n const settings2 = await db.getSettings();\n const pc = (settings2 as any)?.modelPricingConfig || {};\n const savedKeys = pc.providerApiKeys || {};\n const encKey = savedKeys[body.ai_provider];\n if (encKey) resolvedKey = vault.decrypt(encKey);\n }\n await edb()?.run(\n 'INSERT INTO poly_watcher_config (agent_id, ai_provider, ai_model, ai_api_key, analysis_budget_daily, max_spawn_per_hour, use_org_key) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [agentId, body.ai_provider || 'xai', body.ai_model || 'grok-3-mini', resolvedKey, body.analysis_budget_daily || 100, body.max_spawn_per_hour || 6, body.use_org_key ? 1 : 0]\n );\n }\n return c.json({ success: true });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // ─── Polymarket: Watcher Engine Status & Control ──────────\n api.get('/polymarket/engine/status', requireRole('admin'), async (c) => {\n try {\n const status = getWatcherEngineStatus();\n // Also get counts from DB\n const totalWatchers = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watchers').catch(() => ({ cnt: 0 }));\n const activeWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'active'`).catch(() => ({ cnt: 0 }));\n const pausedWatchers = await edb()?.get(`SELECT COUNT(*) as cnt FROM poly_watchers WHERE status = 'paused'`).catch(() => ({ cnt: 0 }));\n const totalEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events').catch(() => ({ cnt: 0 }));\n const unackEvents = await edb()?.get('SELECT COUNT(*) as cnt FROM poly_watcher_events WHERE acknowledged = 0').catch(() => ({ cnt: 0 }));\n return c.json({\n ...status,\n totalWatchers: totalWatchers?.cnt || 0,\n activeWatchers: activeWatchers?.cnt || 0,\n pausedWatchers: pausedWatchers?.cnt || 0,\n totalEvents: totalEvents?.cnt || 0,\n unacknowledgedEvents: unackEvents?.cnt || 0,\n });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n api.post('/polymarket/engine/control', requireRole('admin'), async (c) => {\n try {\n const body = await c.req.json();\n const action = body.action; // 'start' | 'stop'\n if (action !== 'start' && action !== 'stop') return c.json({ error: 'Invalid action' }, 400);\n controlWatcherEngine(action);\n return c.json({ success: true, ...getWatcherEngineStatus() });\n } catch (e: any) { return c.json({ error: e.message }, 500); }\n });\n\n // Auto-connect polymarket proxy 5s after routes are registered\n setTimeout(async () => {\n try {\n await ensurePolyDB();\n await autoConnectProxy(edb());\n } catch (e: any) {\n console.warn(`[polymarket-proxy] Startup auto-connect failed: ${e.message}`);\n }\n }, 5000);\n\n return api;\n}\n"}}}}